Update fullsdk to 4575844
/google/data/ro/projects/android/fetch_artifact \
--bid 4575844 \
--target sdk_phone_x86_64-sdk \
sdk-repo-linux-sources-4575844.zip
Test: TreeHugger
Change-Id: I81e0eb157b4ac3b38408d0ef86f9d6286471f87a
diff --git a/android/accessibilityservice/AccessibilityService.java b/android/accessibilityservice/AccessibilityService.java
index 97dcb90..0a4541b 100644
--- a/android/accessibilityservice/AccessibilityService.java
+++ b/android/accessibilityservice/AccessibilityService.java
@@ -363,6 +363,11 @@
*/
public static final int GLOBAL_ACTION_LOCK_SCREEN = 8;
+ /**
+ * Action to take a screenshot
+ */
+ public static final int GLOBAL_ACTION_TAKE_SCREENSHOT = 9;
+
private static final String LOG_TAG = "AccessibilityService";
/**
diff --git a/android/annotation/SystemApi.java b/android/annotation/SystemApi.java
index 55028eb..e96ff01 100644
--- a/android/annotation/SystemApi.java
+++ b/android/annotation/SystemApi.java
@@ -39,6 +39,6 @@
* @hide
*/
@Target({TYPE, FIELD, METHOD, CONSTRUCTOR, ANNOTATION_TYPE, PACKAGE})
-@Retention(RetentionPolicy.SOURCE)
+@Retention(RetentionPolicy.RUNTIME)
public @interface SystemApi {
}
diff --git a/android/app/Activity.java b/android/app/Activity.java
index aa099eb..cd029c0 100644
--- a/android/app/Activity.java
+++ b/android/app/Activity.java
@@ -16,6 +16,8 @@
package android.app;
+import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS;
+
import static java.lang.Character.MIN_VALUE;
import android.annotation.CallSuper;
@@ -98,6 +100,7 @@
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
+import android.view.RemoteAnimationDefinition;
import android.view.SearchEvent;
import android.view.View;
import android.view.View.OnCreateContextMenuListener;
@@ -857,6 +860,7 @@
private boolean mHasCurrentPermissionsRequest;
private boolean mAutoFillResetNeeded;
+ private boolean mAutoFillIgnoreFirstResumePause;
/** The last autofill id that was returned from {@link #getNextAutofillId()} */
private int mLastAutofillId = View.LAST_APP_AUTOFILL_ID;
@@ -1253,10 +1257,7 @@
getApplication().dispatchActivityStarted(this);
if (mAutoFillResetNeeded) {
- AutofillManager afm = getAutofillManager();
- if (afm != null) {
- afm.onVisibleForAutofill();
- }
+ getAutofillManager().onVisibleForAutofill();
}
}
@@ -1320,6 +1321,20 @@
if (DEBUG_LIFECYCLE) Slog.v(TAG, "onResume " + this);
getApplication().dispatchActivityResumed(this);
mActivityTransitionState.onResume(this, isTopOfTask());
+ if (mAutoFillResetNeeded) {
+ if (!mAutoFillIgnoreFirstResumePause) {
+ View focus = getCurrentFocus();
+ if (focus != null && focus.canNotifyAutofillEnterExitEvent()) {
+ // TODO: in Activity killed/recreated case, i.e. SessionLifecycleTest#
+ // testDatasetVisibleWhileAutofilledAppIsLifecycled: the View's initial
+ // window visibility after recreation is INVISIBLE in onResume() and next frame
+ // ViewRootImpl.performTraversals() changes window visibility to VISIBLE.
+ // So we cannot call View.notifyEnterOrExited() which will do nothing
+ // when View.isVisibleToUser() is false.
+ getAutofillManager().notifyViewEntered(focus);
+ }
+ }
+ }
mCalled = true;
}
@@ -1681,6 +1696,19 @@
protected void onPause() {
if (DEBUG_LIFECYCLE) Slog.v(TAG, "onPause " + this);
getApplication().dispatchActivityPaused(this);
+ if (mAutoFillResetNeeded) {
+ if (!mAutoFillIgnoreFirstResumePause) {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "autofill notifyViewExited " + this);
+ View focus = getCurrentFocus();
+ if (focus != null && focus.canNotifyAutofillEnterExitEvent()) {
+ getAutofillManager().notifyViewExited(focus);
+ }
+ } else {
+ // reset after first pause()
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "autofill got first pause " + this);
+ mAutoFillIgnoreFirstResumePause = false;
+ }
+ }
mCalled = true;
}
@@ -1871,6 +1899,10 @@
mTranslucentCallback = null;
mCalled = true;
+ if (mAutoFillResetNeeded) {
+ getAutofillManager().onInvisibleForAutofill();
+ }
+
if (isFinishing()) {
if (mAutoFillResetNeeded) {
getAutofillManager().onActivityFinished();
@@ -2587,6 +2619,7 @@
* @param id the ID to search for
* @return a view with given ID if found, or {@code null} otherwise
* @see View#findViewById(int)
+ * @see Activity#requireViewById(int)
*/
@Nullable
public <T extends View> T findViewById(@IdRes int id) {
@@ -2594,6 +2627,30 @@
}
/**
+ * Finds a view that was identified by the {@code android:id} XML attribute that was processed
+ * in {@link #onCreate}, or throws an IllegalArgumentException if the ID is invalid, or there is
+ * no matching view in the hierarchy.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see View#requireViewById(int)
+ * @see Activity#findViewById(int)
+ */
+ @NonNull
+ public final <T extends View> T requireViewById(@IdRes int id) {
+ T view = findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException("ID does not reference a View inside this Activity");
+ }
+ return view;
+ }
+
+ /**
* Retrieve a reference to this activity's ActionBar.
*
* @return The Activity's ActionBar, or null if it does not have one.
@@ -4640,6 +4697,7 @@
* their launch had come from the original activity.
* @param intent The Intent to start.
* @param options ActivityOptions or null.
+ * @param permissionToken Token received from the system that permits this call to be made.
* @param ignoreTargetSecurity If true, the activity manager will not check whether the
* caller it is doing the start is, is actually allowed to start the target activity.
* If you set this to true, you must set an explicit component in the Intent and do any
@@ -4648,7 +4706,7 @@
* @hide
*/
public void startActivityAsCaller(Intent intent, @Nullable Bundle options,
- boolean ignoreTargetSecurity, int userId) {
+ IBinder permissionToken, boolean ignoreTargetSecurity, int userId) {
if (mParent != null) {
throw new RuntimeException("Can't be called from a child");
}
@@ -4656,7 +4714,7 @@
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivityAsCaller(
this, mMainThread.getApplicationThread(), mToken, this,
- intent, -1, options, ignoreTargetSecurity, userId);
+ intent, -1, options, permissionToken, ignoreTargetSecurity, userId);
if (ar != null) {
mMainThread.sendActivityResult(
mToken, mEmbeddedID, -1, ar.getResultCode(),
@@ -6266,7 +6324,7 @@
mHandler.getLooper().dump(new PrintWriterPrinter(writer), prefix);
- final AutofillManager afm = getAutofillManager();
+ final AutofillManager afm = mAutofillManager;
if (afm != null) {
afm.dump(prefix, writer);
} else {
@@ -6616,7 +6674,6 @@
* to run as a {@link android.service.vr.VrListenerService} is not installed, or has
* not been enabled in user settings.
*
- * @see android.content.pm.PackageManager#FEATURE_VR_MODE
* @see android.content.pm.PackageManager#FEATURE_VR_MODE_HIGH_PERFORMANCE
* @see android.service.vr.VrListenerService
* @see android.provider.Settings#ACTION_VR_LISTENER_SETTINGS
@@ -7120,13 +7177,23 @@
}
}
- final void performResume() {
+ final void performResume(boolean followedByPause) {
performRestart(true /* start */);
mFragments.execPendingActions();
mLastNonConfigurationInstances = null;
+ if (mAutoFillResetNeeded) {
+ // When Activity is destroyed in paused state, and relaunch activity, there will be
+ // extra onResume and onPause event, ignore the first onResume and onPause.
+ // see ActivityThread.handleRelaunchActivity()
+ mAutoFillIgnoreFirstResumePause = followedByPause;
+ if (mAutoFillIgnoreFirstResumePause && DEBUG_LIFECYCLE) {
+ Slog.v(TAG, "autofill will ignore first pause when relaunching " + this);
+ }
+ }
+
mCalled = false;
// mResumed is set by the instrumentation
mInstrumentation.callActivityOnResume(this);
@@ -7311,7 +7378,7 @@
}
} else if (who.startsWith(AUTO_FILL_AUTH_WHO_PREFIX)) {
Intent resultData = (resultCode == Activity.RESULT_OK) ? data : null;
- getAutofillManager().onAuthenticationResult(requestCode, resultData);
+ getAutofillManager().onAuthenticationResult(requestCode, resultData, getCurrentFocus());
} else {
Fragment frag = mFragments.findFragmentByWho(who);
if (frag != null) {
@@ -7585,6 +7652,12 @@
return !mStopped;
}
+ /** @hide */
+ @Override
+ public boolean isDisablingEnterExitEventForAutofill() {
+ return mAutoFillIgnoreFirstResumePause || !mResumed;
+ }
+
/**
* If set to true, this indicates to the system that it should never take a
* screenshot of the activity to be used as a representation while it is not in a started state.
@@ -7659,6 +7732,22 @@
}
}
+ /**
+ * Registers remote animations per transition type for this activity.
+ *
+ * @param definition The remote animation definition that defines which transition whould run
+ * which remote animation.
+ * @hide
+ */
+ @RequiresPermission(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS)
+ public void registerRemoteAnimations(RemoteAnimationDefinition definition) {
+ try {
+ ActivityManager.getService().registerRemoteAnimations(mToken, definition);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to call registerRemoteAnimations", e);
+ }
+ }
+
class HostCallbacks extends FragmentHostCallback<Activity> {
public HostCallbacks() {
super(Activity.this /*activity*/);
diff --git a/android/app/ActivityManager.java b/android/app/ActivityManager.java
index 1adae7a..8035058 100644
--- a/android/app/ActivityManager.java
+++ b/android/app/ActivityManager.java
@@ -60,6 +60,7 @@
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.os.WorkSource;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.DisplayMetrics;
@@ -180,7 +181,8 @@
BUGREPORT_OPTION_INTERACTIVE,
BUGREPORT_OPTION_REMOTE,
BUGREPORT_OPTION_WEAR,
- BUGREPORT_OPTION_TELEPHONY
+ BUGREPORT_OPTION_TELEPHONY,
+ BUGREPORT_OPTION_WIFI
})
public @interface BugreportMode {}
/**
@@ -215,6 +217,12 @@
public static final int BUGREPORT_OPTION_TELEPHONY = 4;
/**
+ * Takes a lightweight bugreport that only includes a few sections related to Wifi.
+ * @hide
+ */
+ public static final int BUGREPORT_OPTION_WIFI = 5;
+
+ /**
* <a href="{@docRoot}guide/topics/manifest/meta-data-element.html">{@code
* <meta-data>}</a> name for a 'home' Activity that declares a package that is to be
* uninstalled in lieu of the declaring one. The package named here must be
@@ -442,6 +450,31 @@
*/
public static final int INTENT_SENDER_FOREGROUND_SERVICE = 5;
+ /**
+ * Extra included on intents that are delegating the call to
+ * ActivityManager#startActivityAsCaller to another app. This token is necessary for that call
+ * to succeed. Type is IBinder.
+ * @hide
+ */
+ public static final String EXTRA_PERMISSION_TOKEN = "android.app.extra.PERMISSION_TOKEN";
+
+ /**
+ * Extra included on intents that contain an EXTRA_INTENT, with options that the contained
+ * intent may want to be started with. Type is Bundle.
+ * TODO: remove once the ChooserActivity moves to systemui
+ * @hide
+ */
+ public static final String EXTRA_OPTIONS = "android.app.extra.OPTIONS";
+
+ /**
+ * Extra included on intents that contain an EXTRA_INTENT, use this boolean value for the
+ * parameter of the same name when starting the contained intent.
+ * TODO: remove once the ChooserActivity moves to systemui
+ * @hide
+ */
+ public static final String EXTRA_IGNORE_TARGET_SECURITY =
+ "android.app.extra.EXTRA_IGNORE_TARGET_SECURITY";
+
/** @hide User operation call: success! */
public static final int USER_OP_SUCCESS = 0;
@@ -484,11 +517,11 @@
* all activities that are visible to the user. */
public static final int PROCESS_STATE_TOP = 2;
- /** @hide Process is hosting a foreground service due to a system binding. */
- public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 3;
-
/** @hide Process is hosting a foreground service. */
- public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4;
+ public static final int PROCESS_STATE_FOREGROUND_SERVICE = 3;
+
+ /** @hide Process is hosting a foreground service due to a system binding. */
+ public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 4;
/** @hide Process is important to the user, and something they are aware of. */
public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 5;
@@ -3085,11 +3118,11 @@
} else if (importance >= IMPORTANCE_VISIBLE) {
return PROCESS_STATE_IMPORTANT_FOREGROUND;
} else if (importance >= IMPORTANCE_TOP_SLEEPING_PRE_28) {
- return PROCESS_STATE_FOREGROUND_SERVICE;
+ return PROCESS_STATE_IMPORTANT_FOREGROUND;
} else if (importance >= IMPORTANCE_FOREGROUND_SERVICE) {
return PROCESS_STATE_FOREGROUND_SERVICE;
} else {
- return PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+ return PROCESS_STATE_TOP;
}
}
@@ -3911,10 +3944,10 @@
/**
* @hide
*/
- public static void noteWakeupAlarm(PendingIntent ps, int sourceUid, String sourcePkg,
- String tag) {
+ public static void noteWakeupAlarm(PendingIntent ps, WorkSource workSource, int sourceUid,
+ String sourcePkg, String tag) {
try {
- getService().noteWakeupAlarm((ps != null) ? ps.getTarget() : null,
+ getService().noteWakeupAlarm((ps != null) ? ps.getTarget() : null, workSource,
sourceUid, sourcePkg, tag);
} catch (RemoteException ex) {
}
@@ -3923,19 +3956,24 @@
/**
* @hide
*/
- public static void noteAlarmStart(PendingIntent ps, int sourceUid, String tag) {
+ public static void noteAlarmStart(PendingIntent ps, WorkSource workSource, int sourceUid,
+ String tag) {
try {
- getService().noteAlarmStart((ps != null) ? ps.getTarget() : null, sourceUid, tag);
+ getService().noteAlarmStart((ps != null) ? ps.getTarget() : null, workSource,
+ sourceUid, tag);
} catch (RemoteException ex) {
}
}
+
/**
* @hide
*/
- public static void noteAlarmFinish(PendingIntent ps, int sourceUid, String tag) {
+ public static void noteAlarmFinish(PendingIntent ps, WorkSource workSource, int sourceUid,
+ String tag) {
try {
- getService().noteAlarmFinish((ps != null) ? ps.getTarget() : null, sourceUid, tag);
+ getService().noteAlarmFinish((ps != null) ? ps.getTarget() : null, workSource,
+ sourceUid, tag);
} catch (RemoteException ex) {
}
}
diff --git a/android/app/ActivityManagerInternal.java b/android/app/ActivityManagerInternal.java
index 60a5a11..da9f728 100644
--- a/android/app/ActivityManagerInternal.java
+++ b/android/app/ActivityManagerInternal.java
@@ -319,4 +319,29 @@
}
public abstract void registerScreenObserver(ScreenObserver observer);
+
+ /**
+ * Returns if more users can be started without stopping currently running users.
+ */
+ public abstract boolean canStartMoreUsers();
+
+ /**
+ * Sets the user switcher message for switching from {@link android.os.UserHandle#SYSTEM}.
+ */
+ public abstract void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage);
+
+ /**
+ * Sets the user switcher message for switching to {@link android.os.UserHandle#SYSTEM}.
+ */
+ public abstract void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage);
+
+ /**
+ * Returns maximum number of users that can run simultaneously.
+ */
+ public abstract int getMaxRunningUsers();
+
+ /**
+ * Returns is the caller has the same uid as the Recents component
+ */
+ public abstract boolean isCallerRecents(int callingUid);
}
diff --git a/android/app/ActivityManagerNative.java b/android/app/ActivityManagerNative.java
index c09403c..4c558f3 100644
--- a/android/app/ActivityManagerNative.java
+++ b/android/app/ActivityManagerNative.java
@@ -75,20 +75,20 @@
*/
static public void noteWakeupAlarm(PendingIntent ps, int sourceUid, String sourcePkg,
String tag) {
- ActivityManager.noteWakeupAlarm(ps, sourceUid, sourcePkg, tag);
+ ActivityManager.noteWakeupAlarm(ps, null, sourceUid, sourcePkg, tag);
}
/**
* @deprecated use ActivityManager.noteAlarmStart instead.
*/
static public void noteAlarmStart(PendingIntent ps, int sourceUid, String tag) {
- ActivityManager.noteAlarmStart(ps, sourceUid, tag);
+ ActivityManager.noteAlarmStart(ps, null, sourceUid, tag);
}
/**
* @deprecated use ActivityManager.noteAlarmFinish instead.
*/
static public void noteAlarmFinish(PendingIntent ps, int sourceUid, String tag) {
- ActivityManager.noteAlarmFinish(ps, sourceUid, tag);
+ ActivityManager.noteAlarmFinish(ps, null, sourceUid, tag);
}
}
diff --git a/android/app/ActivityOptions.java b/android/app/ActivityOptions.java
index e61c5b7..fee5827 100644
--- a/android/app/ActivityOptions.java
+++ b/android/app/ActivityOptions.java
@@ -16,12 +16,14 @@
package android.app;
+import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS;
import static android.app.ActivityManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.Display.INVALID_DISPLAY;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.annotation.TestApi;
import android.content.ComponentName;
import android.content.Context;
@@ -44,6 +46,7 @@
import android.util.Slog;
import android.view.AppTransitionAnimationSpec;
import android.view.IAppTransitionAnimationSpecsFuture;
+import android.view.RemoteAnimationAdapter;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
@@ -204,6 +207,12 @@
"android.activity.taskOverlayCanResume";
/**
+ * See {@link #setAvoidMoveToFront()}.
+ * @hide
+ */
+ private static final String KEY_AVOID_MOVE_TO_FRONT = "android.activity.avoidMoveToFront";
+
+ /**
* Where the split-screen-primary stack should be positioned.
* @hide
*/
@@ -241,6 +250,8 @@
private static final String KEY_INSTANT_APP_VERIFICATION_BUNDLE
= "android:instantapps.installerbundle";
private static final String KEY_SPECS_FUTURE = "android:activity.specsFuture";
+ private static final String KEY_REMOTE_ANIMATION_ADAPTER
+ = "android:activity.remoteAnimationAdapter";
/** @hide */
public static final int ANIM_NONE = 0;
@@ -268,6 +279,8 @@
public static final int ANIM_CLIP_REVEAL = 11;
/** @hide */
public static final int ANIM_OPEN_CROSS_PROFILE_APPS = 12;
+ /** @hide */
+ public static final int ANIM_REMOTE_ANIMATION = 13;
private String mPackageName;
private Rect mLaunchBounds;
@@ -300,10 +313,12 @@
private boolean mDisallowEnterPictureInPictureWhileLaunching;
private boolean mTaskOverlay;
private boolean mTaskOverlayCanResume;
+ private boolean mAvoidMoveToFront;
private AppTransitionAnimationSpec mAnimSpecs[];
private int mRotationAnimationHint = -1;
private Bundle mAppVerificationBundle;
private IAppTransitionAnimationSpecsFuture mSpecsFuture;
+ private RemoteAnimationAdapter mRemoteAnimationAdapter;
/**
* Create an ActivityOptions specifying a custom animation to run when
@@ -826,6 +841,20 @@
return opts;
}
+ /**
+ * Create an {@link ActivityOptions} instance that lets the application control the entire
+ * animation using a {@link RemoteAnimationAdapter}.
+ * @hide
+ */
+ @RequiresPermission(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS)
+ public static ActivityOptions makeRemoteAnimation(
+ RemoteAnimationAdapter remoteAnimationAdapter) {
+ final ActivityOptions opts = new ActivityOptions();
+ opts.mRemoteAnimationAdapter = remoteAnimationAdapter;
+ opts.mAnimationType = ANIM_REMOTE_ANIMATION;
+ return opts;
+ }
+
/** @hide */
public boolean getLaunchTaskBehind() {
return mAnimationType == ANIM_LAUNCH_TASK_BEHIND;
@@ -901,6 +930,7 @@
mLaunchTaskId = opts.getInt(KEY_LAUNCH_TASK_ID, -1);
mTaskOverlay = opts.getBoolean(KEY_TASK_OVERLAY, false);
mTaskOverlayCanResume = opts.getBoolean(KEY_TASK_OVERLAY_CAN_RESUME, false);
+ mAvoidMoveToFront = opts.getBoolean(KEY_AVOID_MOVE_TO_FRONT, false);
mSplitScreenCreateMode = opts.getInt(KEY_SPLIT_SCREEN_CREATE_MODE,
SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT);
mDisallowEnterPictureInPictureWhileLaunching = opts.getBoolean(
@@ -922,6 +952,7 @@
mSpecsFuture = IAppTransitionAnimationSpecsFuture.Stub.asInterface(opts.getBinder(
KEY_SPECS_FUTURE));
}
+ mRemoteAnimationAdapter = opts.getParcelable(KEY_REMOTE_ANIMATION_ADAPTER);
}
/**
@@ -1070,6 +1101,11 @@
}
/** @hide */
+ public RemoteAnimationAdapter getRemoteAnimationAdapter() {
+ return mRemoteAnimationAdapter;
+ }
+
+ /** @hide */
public static ActivityOptions fromBundle(Bundle bOptions) {
return bOptions != null ? new ActivityOptions(bOptions) : null;
}
@@ -1211,6 +1247,25 @@
return mTaskOverlayCanResume;
}
+ /**
+ * Sets whether the activity launched should not cause the activity stack it is contained in to
+ * be moved to the front as a part of launching.
+ *
+ * @hide
+ */
+ public void setAvoidMoveToFront() {
+ mAvoidMoveToFront = true;
+ }
+
+ /**
+ * @return whether the activity launch should prevent moving the associated activity stack to
+ * the front.
+ * @hide
+ */
+ public boolean getAvoidMoveToFront() {
+ return mAvoidMoveToFront;
+ }
+
/** @hide */
public int getSplitScreenCreateMode() {
return mSplitScreenCreateMode;
@@ -1309,6 +1364,7 @@
mAnimSpecs = otherOptions.mAnimSpecs;
mAnimationFinishedListener = otherOptions.mAnimationFinishedListener;
mSpecsFuture = otherOptions.mSpecsFuture;
+ mRemoteAnimationAdapter = otherOptions.mRemoteAnimationAdapter;
}
/**
@@ -1387,6 +1443,7 @@
b.putInt(KEY_LAUNCH_TASK_ID, mLaunchTaskId);
b.putBoolean(KEY_TASK_OVERLAY, mTaskOverlay);
b.putBoolean(KEY_TASK_OVERLAY_CAN_RESUME, mTaskOverlayCanResume);
+ b.putBoolean(KEY_AVOID_MOVE_TO_FRONT, mAvoidMoveToFront);
b.putInt(KEY_SPLIT_SCREEN_CREATE_MODE, mSplitScreenCreateMode);
b.putBoolean(KEY_DISALLOW_ENTER_PICTURE_IN_PICTURE_WHILE_LAUNCHING,
mDisallowEnterPictureInPictureWhileLaunching);
@@ -1403,7 +1460,9 @@
if (mAppVerificationBundle != null) {
b.putBundle(KEY_INSTANT_APP_VERIFICATION_BUNDLE, mAppVerificationBundle);
}
-
+ if (mRemoteAnimationAdapter != null) {
+ b.putParcelable(KEY_REMOTE_ANIMATION_ADAPTER, mRemoteAnimationAdapter);
+ }
return b;
}
diff --git a/android/app/ActivityThread.java b/android/app/ActivityThread.java
index aaa6bf0..934b0f3 100644
--- a/android/app/ActivityThread.java
+++ b/android/app/ActivityThread.java
@@ -166,6 +166,7 @@
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.text.DateFormat;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -220,6 +221,9 @@
// Whether to invoke an activity callback after delivering new configuration.
private static final boolean REPORT_TO_ACTIVITY = true;
+ // Maximum number of recent tokens to maintain for debugging purposes
+ private static final int MAX_RECENT_TOKENS = 10;
+
/**
* Denotes an invalid sequence number corresponding to a process state change.
*/
@@ -252,6 +256,8 @@
final H mH = new H();
final Executor mExecutor = new HandlerExecutor(mH);
final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();
+ final ArrayDeque<Integer> mRecentTokens = new ArrayDeque<>();
+
// List of new activities (via ActivityRecord.nextIdle) that should
// be reported when next we idle.
ActivityClientRecord mNewActivities = null;
@@ -1752,9 +1758,11 @@
handleLocalVoiceInteractionStarted((IBinder) ((SomeArgs) msg.obj).arg1,
(IVoiceInteractor) ((SomeArgs) msg.obj).arg2);
break;
- case ATTACH_AGENT:
- handleAttachAgent((String) msg.obj);
+ case ATTACH_AGENT: {
+ Application app = getApplication();
+ handleAttachAgent((String) msg.obj, app != null ? app.mLoadedApk : null);
break;
+ }
case APPLICATION_INFO_CHANGED:
mUpdatingSystemConfig = true;
try {
@@ -1770,7 +1778,12 @@
case EXECUTE_TRANSACTION:
final ClientTransaction transaction = (ClientTransaction) msg.obj;
mTransactionExecutor.execute(transaction);
- transaction.recycle();
+ if (isSystem()) {
+ // Client transactions inside system process are recycled on the client side
+ // instead of ClientLifecycleManager to avoid being cleared before this
+ // message is handled.
+ transaction.recycle();
+ }
break;
}
Object obj = msg.obj;
@@ -2161,6 +2174,18 @@
pw.println(String.format(format, objs));
}
+ @Override
+ public void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + "mActivities:");
+
+ for (ArrayMap.Entry<IBinder, ActivityClientRecord> entry : mActivities.entrySet()) {
+ pw.println(prefix + " [token:" + entry.getKey().hashCode() + " record:"
+ + entry.getValue().toString() + "]");
+ }
+
+ pw.println(prefix + "mRecentTokens:" + mRecentTokens);
+ }
+
public static void dumpMemInfoTable(PrintWriter pw, Debug.MemoryInfo memInfo, boolean checkin,
boolean dumpFullInfo, boolean dumpDalvik, boolean dumpSummaryOnly,
int pid, String processName,
@@ -2845,6 +2870,11 @@
r.setState(ON_CREATE);
mActivities.put(r.token, r);
+ mRecentTokens.push(r.token.hashCode());
+
+ if (mRecentTokens.size() > MAX_RECENT_TOKENS) {
+ mRecentTokens.removeLast();
+ }
} catch (SuperNotCalledException e) {
throw e;
@@ -3066,7 +3096,7 @@
checkAndBlockForNetworkAccess();
deliverNewIntents(r, intents);
if (resumed) {
- r.activity.performResume();
+ r.activity.performResume(false);
r.activity.mTemporaryPause = false;
}
@@ -3241,11 +3271,23 @@
}
}
- static final void handleAttachAgent(String agent) {
+ private static boolean attemptAttachAgent(String agent, ClassLoader classLoader) {
try {
- VMDebug.attachAgent(agent);
+ VMDebug.attachAgent(agent, classLoader);
+ return true;
} catch (IOException e) {
- Slog.e(TAG, "Attaching agent failed: " + agent);
+ Slog.e(TAG, "Attaching agent with " + classLoader + " failed: " + agent);
+ return false;
+ }
+ }
+
+ static void handleAttachAgent(String agent, LoadedApk loadedApk) {
+ ClassLoader classLoader = loadedApk != null ? loadedApk.getClassLoader() : null;
+ if (attemptAttachAgent(agent, classLoader)) {
+ return;
+ }
+ if (classLoader != null) {
+ attemptAttachAgent(agent, null);
}
}
@@ -3676,7 +3718,7 @@
deliverResults(r, r.pendingResults);
r.pendingResults = null;
}
- r.activity.performResume();
+ r.activity.performResume(r.startsNotResumed);
synchronized (mResourcesManager) {
// If there is a pending local relaunch that was requested when the activity was
@@ -4395,7 +4437,7 @@
checkAndBlockForNetworkAccess();
deliverResults(r, results);
if (resumed) {
- r.activity.performResume();
+ r.activity.performResume(false);
r.activity.mTemporaryPause = false;
}
}
@@ -5537,12 +5579,16 @@
mCompatConfiguration = new Configuration(data.config);
mProfiler = new Profiler();
+ String agent = null;
if (data.initProfilerInfo != null) {
mProfiler.profileFile = data.initProfilerInfo.profileFile;
mProfiler.profileFd = data.initProfilerInfo.profileFd;
mProfiler.samplingInterval = data.initProfilerInfo.samplingInterval;
mProfiler.autoStopProfiler = data.initProfilerInfo.autoStopProfiler;
mProfiler.streamingOutput = data.initProfilerInfo.streamingOutput;
+ if (data.initProfilerInfo.attachAgentDuringBind) {
+ agent = data.initProfilerInfo.agent;
+ }
}
// send up app name; do this *before* waiting for debugger
@@ -5592,6 +5638,10 @@
data.loadedApk = getLoadedApkNoCheck(data.appInfo, data.compatInfo);
+ if (agent != null) {
+ handleAttachAgent(agent, data.loadedApk);
+ }
+
/**
* Switch this process to density compatibility mode if needed.
*/
diff --git a/android/app/ActivityView.java b/android/app/ActivityView.java
index 9f1e983..5d0143a 100644
--- a/android/app/ActivityView.java
+++ b/android/app/ActivityView.java
@@ -17,6 +17,7 @@
package android.app;
import android.annotation.NonNull;
+import android.app.ActivityManager.StackInfo;
import android.content.Context;
import android.content.Intent;
import android.hardware.display.DisplayManager;
@@ -34,9 +35,12 @@
import android.view.SurfaceView;
import android.view.ViewGroup;
import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
import dalvik.system.CloseGuard;
+import java.util.List;
+
/**
* Activity container that allows launching activities into itself and does input forwarding.
* <p>Creation of this view is only allowed to callers who have
@@ -57,7 +61,12 @@
private final SurfaceCallback mSurfaceCallback;
private StateCallback mActivityViewCallback;
+ private IActivityManager mActivityManager;
private IInputForwarder mInputForwarder;
+ // Temp container to store view coordinates on screen.
+ private final int[] mLocationOnScreen = new int[2];
+
+ private TaskStackListener mTaskStackListener;
private final CloseGuard mGuard = CloseGuard.get();
private boolean mOpened; // Protected by mGuard.
@@ -73,6 +82,7 @@
public ActivityView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+ mActivityManager = ActivityManager.getService();
mSurfaceView = new SurfaceView(context);
mSurfaceCallback = new SurfaceCallback();
mSurfaceView.getHolder().addCallback(mSurfaceCallback);
@@ -198,11 +208,30 @@
performRelease();
}
+ /**
+ * Triggers an update of {@link ActivityView}'s location on screen to properly set touch exclude
+ * regions and avoid focus switches by touches on this view.
+ */
+ public void onLocationChanged() {
+ updateLocation();
+ }
+
@Override
public void onLayout(boolean changed, int l, int t, int r, int b) {
mSurfaceView.layout(0 /* left */, 0 /* top */, r - l /* right */, b - t /* bottom */);
}
+ /** Send current location and size to the WM to set tap exclude region for this view. */
+ private void updateLocation() {
+ try {
+ getLocationOnScreen(mLocationOnScreen);
+ WindowManagerGlobal.getWindowSession().updateTapExcludeRegion(getWindow(), hashCode(),
+ mLocationOnScreen[0], mLocationOnScreen[1], getWidth(), getHeight());
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
@Override
public boolean onTouchEvent(MotionEvent event) {
return injectInputEvent(event) || super.onTouchEvent(event);
@@ -241,6 +270,7 @@
} else {
mVirtualDisplay.setSurface(surfaceHolder.getSurface());
}
+ updateLocation();
}
@Override
@@ -248,6 +278,7 @@
if (mVirtualDisplay != null) {
mVirtualDisplay.resize(width, height, getBaseDisplayDensity());
}
+ updateLocation();
}
@Override
@@ -257,6 +288,7 @@
if (mVirtualDisplay != null) {
mVirtualDisplay.setSurface(null);
}
+ cleanTapExcludeRegion();
}
}
@@ -278,6 +310,12 @@
mInputForwarder = InputManager.getInstance().createInputForwarder(
mVirtualDisplay.getDisplay().getDisplayId());
+ mTaskStackListener = new TaskBackgroundChangeListener();
+ try {
+ mActivityManager.registerTaskStackListener(mTaskStackListener);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to register task stack listener", e);
+ }
}
private void performRelease() {
@@ -290,6 +328,16 @@
if (mInputForwarder != null) {
mInputForwarder = null;
}
+ cleanTapExcludeRegion();
+
+ if (mTaskStackListener != null) {
+ try {
+ mActivityManager.unregisterTaskStackListener(mTaskStackListener);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to unregister task stack listener", e);
+ }
+ mTaskStackListener = null;
+ }
final boolean displayReleased;
if (mVirtualDisplay != null) {
@@ -313,6 +361,17 @@
mOpened = false;
}
+ /** Report to server that tap exclude region on hosting display should be cleared. */
+ private void cleanTapExcludeRegion() {
+ // Update tap exclude region with an empty rect to clean the state on server.
+ try {
+ WindowManagerGlobal.getWindowSession().updateTapExcludeRegion(getWindow(), hashCode(),
+ 0 /* left */, 0 /* top */, 0 /* width */, 0 /* height */);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
/** Get density of the hosting display. */
private int getBaseDisplayDensity() {
final WindowManager wm = mContext.getSystemService(WindowManager.class);
@@ -332,4 +391,42 @@
super.finalize();
}
}
+
+ /**
+ * A task change listener that detects background color change of the topmost stack on our
+ * virtual display and updates the background of the surface view. This background will be shown
+ * when surface view is resized, but the app hasn't drawn its content in new size yet.
+ */
+ private class TaskBackgroundChangeListener extends TaskStackListener {
+
+ @Override
+ public void onTaskDescriptionChanged(int taskId, ActivityManager.TaskDescription td)
+ throws RemoteException {
+ if (mVirtualDisplay == null) {
+ return;
+ }
+
+ // Find the topmost task on our virtual display - it will define the background
+ // color of the surface view during resizing.
+ final int displayId = mVirtualDisplay.getDisplay().getDisplayId();
+ final List<StackInfo> stackInfoList = mActivityManager.getAllStackInfos();
+
+ // Iterate through stacks from top to bottom.
+ final int stackCount = stackInfoList.size();
+ for (int i = 0; i < stackCount; i++) {
+ final StackInfo stackInfo = stackInfoList.get(i);
+ // Only look for stacks on our virtual display.
+ if (stackInfo.displayId != displayId) {
+ continue;
+ }
+ // Found the topmost stack on target display. Now check if the topmost task's
+ // description changed.
+ if (taskId == stackInfo.taskIds[stackInfo.taskIds.length - 1]) {
+ mSurfaceView.setResizeBackgroundColor(td.getBackgroundColor());
+ }
+ break;
+ }
+ }
+ }
+
}
diff --git a/android/app/AppOpsManager.java b/android/app/AppOpsManager.java
index ea22d33..e923fb2 100644
--- a/android/app/AppOpsManager.java
+++ b/android/app/AppOpsManager.java
@@ -20,6 +20,7 @@
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.annotation.TestApi;
import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.media.AudioAttributes.AttributeUsage;
@@ -37,6 +38,7 @@
import com.android.internal.app.IAppOpsService;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -106,6 +108,7 @@
// when adding one of these:
// - increment _NUM_OP
+ // - define an OPSTR_* constant (marked as @SystemApi)
// - add rows to sOpToSwitch, sOpToString, sOpNames, sOpToPerms, sOpDefault
// - add descriptive strings to Settings/res/values/arrays.xml
// - add the op to the appropriate template in AppOpsState.OpsTemplate (settings app)
@@ -260,8 +263,10 @@
public static final int OP_REQUEST_DELETE_PACKAGES = 72;
/** @hide Bind an accessibility service. */
public static final int OP_BIND_ACCESSIBILITY_SERVICE = 73;
+ /** @hide Continue handover of a call from another app */
+ public static final int OP_ACCEPT_HANDOVER = 74;
/** @hide */
- public static final int _NUM_OP = 74;
+ public static final int _NUM_OP = 75;
/** Access to coarse location information. */
public static final String OPSTR_COARSE_LOCATION = "android:coarse_location";
@@ -278,7 +283,7 @@
public static final String OPSTR_GET_USAGE_STATS
= "android:get_usage_stats";
/** Activate a VPN connection without user intervention. @hide */
- @SystemApi
+ @SystemApi @TestApi
public static final String OPSTR_ACTIVATE_VPN
= "android:activate_vpn";
/** Allows an application to read the user's contacts data. */
@@ -360,6 +365,7 @@
public static final String OPSTR_WRITE_SETTINGS
= "android:write_settings";
/** @hide Get device accounts. */
+ @SystemApi @TestApi
public static final String OPSTR_GET_ACCOUNTS
= "android:get_accounts";
public static final String OPSTR_READ_PHONE_NUMBERS
@@ -368,11 +374,133 @@
public static final String OPSTR_PICTURE_IN_PICTURE
= "android:picture_in_picture";
/** @hide */
+ @SystemApi @TestApi
public static final String OPSTR_INSTANT_APP_START_FOREGROUND
= "android:instant_app_start_foreground";
/** Answer incoming phone calls */
public static final String OPSTR_ANSWER_PHONE_CALLS
= "android:answer_phone_calls";
+ /**
+ * Accept call handover
+ * @hide
+ */
+ @SystemApi @TestApi
+ public static final String OPSTR_ACCEPT_HANDOVER
+ = "android:accept_handover";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_GPS = "android:gps";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_VIBRATE = "android:vibrate";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_WIFI_SCAN = "android:wifi_scan";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_POST_NOTIFICATION = "android:post_notification";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_NEIGHBORING_CELLS = "android:neighboring_cells";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_WRITE_SMS = "android:write_sms";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_RECEIVE_EMERGENCY_BROADCAST =
+ "android:receive_emergency_broadcast";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_READ_ICC_SMS = "android:read_icc_sms";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_WRITE_ICC_SMS = "android:write_icc_sms";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_ACCESS_NOTIFICATIONS = "android:access_notifications";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_PLAY_AUDIO = "android:play_audio";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_READ_CLIPBOARD = "android:read_clipboard";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_WRITE_CLIPBOARD = "android:write_clipboard";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_TAKE_MEDIA_BUTTONS = "android:take_media_buttons";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_TAKE_AUDIO_FOCUS = "android:take_audio_focus";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_AUDIO_MASTER_VOLUME = "android:audio_master_volume";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_AUDIO_VOICE_VOLUME = "android:audio_voice_volume";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_AUDIO_RING_VOLUME = "android:audio_ring_volume";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_AUDIO_MEDIA_VOLUME = "android:audio_media_volume";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_AUDIO_ALARM_VOLUME = "android:audio_alarm_volume";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_AUDIO_NOTIFICATION_VOLUME =
+ "android:audio_notification_volume";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_AUDIO_BLUETOOTH_VOLUME = "android:audio_bluetooth_volume";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_WAKE_LOCK = "android:wake_lock";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_MUTE_MICROPHONE = "android:mute_microphone";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_TOAST_WINDOW = "android:toast_window";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_PROJECT_MEDIA = "android:project_media";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_WRITE_WALLPAPER = "android:write_wallpaper";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_ASSIST_STRUCTURE = "android:assist_structure";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_ASSIST_SCREENSHOT = "android:assist_screenshot";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_TURN_SCREEN_ON = "android:turn_screen_on";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_RUN_IN_BACKGROUND = "android:run_in_background";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_AUDIO_ACCESSIBILITY_VOLUME =
+ "android:audio_accessibility_volume";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_REQUEST_INSTALL_PACKAGES = "android:request_install_packages";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_RUN_ANY_IN_BACKGROUND = "android:run_any_in_background";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_CHANGE_WIFI_STATE = "change_wifi_state";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_REQUEST_DELETE_PACKAGES = "request_delete_packages";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_BIND_ACCESSIBILITY_SERVICE = "bind_accessibility_service";
// Warning: If an permission is added here it also has to be added to
// com.android.packageinstaller.permission.utils.EventLogger
@@ -408,6 +536,7 @@
OP_USE_SIP,
OP_PROCESS_OUTGOING_CALLS,
OP_ANSWER_PHONE_CALLS,
+ OP_ACCEPT_HANDOVER,
// Microphone
OP_RECORD_AUDIO,
// Camera
@@ -506,64 +635,64 @@
OP_CHANGE_WIFI_STATE,
OP_REQUEST_DELETE_PACKAGES,
OP_BIND_ACCESSIBILITY_SERVICE,
+ OP_ACCEPT_HANDOVER,
};
/**
* This maps each operation to the public string constant for it.
- * If it doesn't have a public string constant, it maps to null.
*/
- private static String[] sOpToString = new String[] {
+ private static String[] sOpToString = new String[]{
OPSTR_COARSE_LOCATION,
OPSTR_FINE_LOCATION,
- null,
- null,
+ OPSTR_GPS,
+ OPSTR_VIBRATE,
OPSTR_READ_CONTACTS,
OPSTR_WRITE_CONTACTS,
OPSTR_READ_CALL_LOG,
OPSTR_WRITE_CALL_LOG,
OPSTR_READ_CALENDAR,
OPSTR_WRITE_CALENDAR,
- null,
- null,
- null,
+ OPSTR_WIFI_SCAN,
+ OPSTR_POST_NOTIFICATION,
+ OPSTR_NEIGHBORING_CELLS,
OPSTR_CALL_PHONE,
OPSTR_READ_SMS,
- null,
+ OPSTR_WRITE_SMS,
OPSTR_RECEIVE_SMS,
- null,
+ OPSTR_RECEIVE_EMERGENCY_BROADCAST,
OPSTR_RECEIVE_MMS,
OPSTR_RECEIVE_WAP_PUSH,
OPSTR_SEND_SMS,
- null,
- null,
+ OPSTR_READ_ICC_SMS,
+ OPSTR_WRITE_ICC_SMS,
OPSTR_WRITE_SETTINGS,
OPSTR_SYSTEM_ALERT_WINDOW,
- null,
+ OPSTR_ACCESS_NOTIFICATIONS,
OPSTR_CAMERA,
OPSTR_RECORD_AUDIO,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
+ OPSTR_PLAY_AUDIO,
+ OPSTR_READ_CLIPBOARD,
+ OPSTR_WRITE_CLIPBOARD,
+ OPSTR_TAKE_MEDIA_BUTTONS,
+ OPSTR_TAKE_AUDIO_FOCUS,
+ OPSTR_AUDIO_MASTER_VOLUME,
+ OPSTR_AUDIO_VOICE_VOLUME,
+ OPSTR_AUDIO_RING_VOLUME,
+ OPSTR_AUDIO_MEDIA_VOLUME,
+ OPSTR_AUDIO_ALARM_VOLUME,
+ OPSTR_AUDIO_NOTIFICATION_VOLUME,
+ OPSTR_AUDIO_BLUETOOTH_VOLUME,
+ OPSTR_WAKE_LOCK,
OPSTR_MONITOR_LOCATION,
OPSTR_MONITOR_HIGH_POWER_LOCATION,
OPSTR_GET_USAGE_STATS,
- null,
- null,
- null,
+ OPSTR_MUTE_MICROPHONE,
+ OPSTR_TOAST_WINDOW,
+ OPSTR_PROJECT_MEDIA,
OPSTR_ACTIVATE_VPN,
- null,
- null,
- null,
+ OPSTR_WRITE_WALLPAPER,
+ OPSTR_ASSIST_STRUCTURE,
+ OPSTR_ASSIST_SCREENSHOT,
OPSTR_READ_PHONE_STATE,
OPSTR_ADD_VOICEMAIL,
OPSTR_USE_SIP,
@@ -574,19 +703,20 @@
OPSTR_MOCK_LOCATION,
OPSTR_READ_EXTERNAL_STORAGE,
OPSTR_WRITE_EXTERNAL_STORAGE,
- null,
+ OPSTR_TURN_SCREEN_ON,
OPSTR_GET_ACCOUNTS,
- null,
- null, // OP_AUDIO_ACCESSIBILITY_VOLUME
+ OPSTR_RUN_IN_BACKGROUND,
+ OPSTR_AUDIO_ACCESSIBILITY_VOLUME,
OPSTR_READ_PHONE_NUMBERS,
- null, // OP_REQUEST_INSTALL_PACKAGES
+ OPSTR_REQUEST_INSTALL_PACKAGES,
OPSTR_PICTURE_IN_PICTURE,
OPSTR_INSTANT_APP_START_FOREGROUND,
OPSTR_ANSWER_PHONE_CALLS,
- null, // OP_RUN_ANY_IN_BACKGROUND
- null, // OP_CHANGE_WIFI_STATE
- null, // OP_REQUEST_DELETE_PACKAGES
- null, // OP_BIND_ACCESSIBILITY_SERVICE
+ OPSTR_RUN_ANY_IN_BACKGROUND,
+ OPSTR_CHANGE_WIFI_STATE,
+ OPSTR_REQUEST_DELETE_PACKAGES,
+ OPSTR_BIND_ACCESSIBILITY_SERVICE,
+ OPSTR_ACCEPT_HANDOVER,
};
/**
@@ -668,6 +798,7 @@
"CHANGE_WIFI_STATE",
"REQUEST_DELETE_PACKAGES",
"BIND_ACCESSIBILITY_SERVICE",
+ "ACCEPT_HANDOVER",
};
/**
@@ -749,6 +880,7 @@
Manifest.permission.CHANGE_WIFI_STATE,
Manifest.permission.REQUEST_DELETE_PACKAGES,
Manifest.permission.BIND_ACCESSIBILITY_SERVICE,
+ Manifest.permission.ACCEPT_HANDOVER,
};
/**
@@ -831,6 +963,7 @@
null, // OP_CHANGE_WIFI_STATE
null, // REQUEST_DELETE_PACKAGES
null, // OP_BIND_ACCESSIBILITY_SERVICE
+ null, // ACCEPT_HANDOVER
};
/**
@@ -912,6 +1045,7 @@
false, // OP_CHANGE_WIFI_STATE
false, // OP_REQUEST_DELETE_PACKAGES
false, // OP_BIND_ACCESSIBILITY_SERVICE
+ false, // ACCEPT_HANDOVER
};
/**
@@ -992,6 +1126,7 @@
AppOpsManager.MODE_ALLOWED, // OP_CHANGE_WIFI_STATE
AppOpsManager.MODE_ALLOWED, // REQUEST_DELETE_PACKAGES
AppOpsManager.MODE_ALLOWED, // OP_BIND_ACCESSIBILITY_SERVICE
+ AppOpsManager.MODE_ALLOWED, // ACCEPT_HANDOVER
};
/**
@@ -1076,6 +1211,7 @@
false, // OP_CHANGE_WIFI_STATE
false, // OP_REQUEST_DELETE_PACKAGES
false, // OP_BIND_ACCESSIBILITY_SERVICE
+ false, // ACCEPT_HANDOVER
};
/**
@@ -1207,6 +1343,25 @@
}
/**
+ * Retrieve the human readable mode.
+ * @hide
+ */
+ public static String modeToString(int mode) {
+ switch (mode) {
+ case MODE_ALLOWED:
+ return "allow";
+ case MODE_IGNORED:
+ return "ignore";
+ case MODE_ERRORED:
+ return "deny";
+ case MODE_DEFAULT:
+ return "default";
+ default:
+ return "mode=" + mode;
+ }
+ }
+
+ /**
* Retrieve whether the op allows itself to be reset.
* @hide
*/
@@ -1482,6 +1637,7 @@
}
/** @hide */
+ @TestApi
public void setMode(int code, int uid, String packageName, int mode) {
try {
mService.setMode(code, uid, packageName, mode);
@@ -1997,4 +2153,14 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Returns all supported operation names.
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public static String[] getOpStrs() {
+ return Arrays.copyOf(sOpToString, sOpToString.length);
+ }
}
diff --git a/android/app/ApplicationPackageManager.java b/android/app/ApplicationPackageManager.java
index 8641a21..cc68c05 100644
--- a/android/app/ApplicationPackageManager.java
+++ b/android/app/ApplicationPackageManager.java
@@ -64,7 +64,6 @@
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
-import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -223,9 +222,18 @@
@Override
public Intent getLeanbackLaunchIntentForPackage(String packageName) {
- // Try to find a main leanback_launcher activity.
+ return getLaunchIntentForPackageAndCategory(packageName, Intent.CATEGORY_LEANBACK_LAUNCHER);
+ }
+
+ @Override
+ public Intent getCarLaunchIntentForPackage(String packageName) {
+ return getLaunchIntentForPackageAndCategory(packageName, Intent.CATEGORY_CAR_LAUNCHER);
+ }
+
+ private Intent getLaunchIntentForPackageAndCategory(String packageName, String category) {
+ // Try to find a main launcher activity for the given categories.
Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
- intentToResolve.addCategory(Intent.CATEGORY_LEANBACK_LAUNCHER);
+ intentToResolve.addCategory(category);
intentToResolve.setPackage(packageName);
List<ResolveInfo> ris = queryIntentActivities(intentToResolve, 0);
@@ -691,6 +699,26 @@
}
@Override
+ public boolean hasSigningCertificate(
+ String packageName, byte[] certificate, @PackageManager.CertificateInputType int type) {
+ try {
+ return mPM.hasSigningCertificate(packageName, certificate, type);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean hasSigningCertificate(
+ int uid, byte[] certificate, @PackageManager.CertificateInputType int type) {
+ try {
+ return mPM.hasUidSigningCertificate(uid, certificate, type);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
public String[] getPackagesForUid(int uid) {
try {
return mPM.getPackagesForUid(uid);
@@ -1683,22 +1711,6 @@
}
@Override
- public void installPackage(Uri packageURI,
- PackageInstallObserver observer, int flags, String installerPackageName) {
- if (!"file".equals(packageURI.getScheme())) {
- throw new UnsupportedOperationException("Only file:// URIs are supported");
- }
-
- final String originPath = packageURI.getPath();
- try {
- mPM.installPackageAsUser(originPath, observer.getBinder(), flags, installerPackageName,
- mContext.getUserId());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- @Override
public int installExistingPackage(String packageName) throws NameNotFoundException {
return installExistingPackage(packageName, PackageManager.INSTALL_REASON_UNKNOWN);
}
@@ -2755,6 +2767,24 @@
}
@Override
+ public CharSequence getHarmfulAppWarning(String packageName) {
+ try {
+ return mPM.getHarmfulAppWarning(packageName, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ @Override
+ public void setHarmfulAppWarning(String packageName, CharSequence warning) {
+ try {
+ mPM.setHarmfulAppWarning(packageName, warning, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ @Override
public ArtManager getArtManager() {
synchronized (mLock) {
if (mArtManager == null) {
diff --git a/android/app/ClientTransactionHandler.java b/android/app/ClientTransactionHandler.java
index 45c0e0c..0f66652 100644
--- a/android/app/ClientTransactionHandler.java
+++ b/android/app/ClientTransactionHandler.java
@@ -24,6 +24,7 @@
import com.android.internal.content.ReferrerIntent;
+import java.io.PrintWriter;
import java.util.List;
/**
@@ -121,4 +122,11 @@
* provided token.
*/
public abstract ActivityThread.ActivityClientRecord getActivityClient(IBinder token);
+
+ /**
+ * Debugging output.
+ * @param pw {@link PrintWriter} to write logs to.
+ * @param prefix Prefix to prepend to output.
+ */
+ public abstract void dump(PrintWriter pw, String prefix);
}
diff --git a/android/app/ContextImpl.java b/android/app/ContextImpl.java
index 1653430..4914ffa 100644
--- a/android/app/ContextImpl.java
+++ b/android/app/ContextImpl.java
@@ -872,13 +872,19 @@
// Calling start activity from outside an activity without FLAG_ACTIVITY_NEW_TASK is
// generally not allowed, except if the caller specifies the task id the activity should
- // be launched in.
- if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0
- && options != null && ActivityOptions.fromBundle(options).getLaunchTaskId() == -1) {
+ // be launched in. A bug was existed between N and O-MR1 which allowed this to work. We
+ // maintain this for backwards compatibility.
+ final int targetSdkVersion = getApplicationInfo().targetSdkVersion;
+
+ if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0
+ && (targetSdkVersion < Build.VERSION_CODES.N
+ || targetSdkVersion >= Build.VERSION_CODES.P)
+ && (options == null
+ || ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) {
throw new AndroidRuntimeException(
"Calling startActivity() from outside of an Activity "
- + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
- + " Is this really what you want?");
+ + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ + " Is this really what you want?");
}
mMainThread.getInstrumentation().execStartActivity(
getOuterContext(), mMainThread.getApplicationThread(), null,
diff --git a/android/app/Dialog.java b/android/app/Dialog.java
index b162cb1..2b648ea 100644
--- a/android/app/Dialog.java
+++ b/android/app/Dialog.java
@@ -16,10 +16,6 @@
package android.app;
-import com.android.internal.R;
-import com.android.internal.app.WindowDecorActionBar;
-import com.android.internal.policy.PhoneWindow;
-
import android.annotation.CallSuper;
import android.annotation.DrawableRes;
import android.annotation.IdRes;
@@ -32,8 +28,8 @@
import android.content.Context;
import android.content.ContextWrapper;
import android.content.DialogInterface;
-import android.content.res.Configuration;
import android.content.pm.ApplicationInfo;
+import android.content.res.Configuration;
import android.content.res.ResourceId;
import android.graphics.drawable.Drawable;
import android.net.Uri;
@@ -62,6 +58,10 @@
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
+import com.android.internal.R;
+import com.android.internal.app.WindowDecorActionBar;
+import com.android.internal.policy.PhoneWindow;
+
import java.lang.ref.WeakReference;
/**
@@ -512,6 +512,7 @@
* @param id the ID to search for
* @return a view with given ID if found, or {@code null} otherwise
* @see View#findViewById(int)
+ * @see Dialog#requireViewById(int)
*/
@Nullable
public <T extends View> T findViewById(@IdRes int id) {
@@ -519,6 +520,30 @@
}
/**
+ * Finds the first descendant view with the given ID or throws an IllegalArgumentException if
+ * the ID is invalid (< 0), there is no matching view in the hierarchy, or the dialog has not
+ * yet been fully created (for example, via {@link #show()} or {@link #create()}).
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see View#requireViewById(int)
+ * @see Dialog#findViewById(int)
+ */
+ @NonNull
+ public final <T extends View> T requireViewById(@IdRes int id) {
+ T view = findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException("ID does not reference a View inside this Dialog");
+ }
+ return view;
+ }
+
+ /**
* Set the screen content from a layout resource. The resource will be
* inflated, adding all top-level views to the screen.
*
diff --git a/android/app/Instrumentation.java b/android/app/Instrumentation.java
index b469de5..3c38a4e 100644
--- a/android/app/Instrumentation.java
+++ b/android/app/Instrumentation.java
@@ -17,6 +17,7 @@
package android.app;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
@@ -458,7 +459,8 @@
*
* @see Context#startActivity(Intent, Bundle)
*/
- public Activity startActivitySync(Intent intent, @Nullable Bundle options) {
+ @NonNull
+ public Activity startActivitySync(@NonNull Intent intent, @Nullable Bundle options) {
validateNotAppThread();
synchronized (mSync) {
@@ -1872,8 +1874,8 @@
*/
public ActivityResult execStartActivityAsCaller(
Context who, IBinder contextThread, IBinder token, Activity target,
- Intent intent, int requestCode, Bundle options, boolean ignoreTargetSecurity,
- int userId) {
+ Intent intent, int requestCode, Bundle options, IBinder permissionToken,
+ boolean ignoreTargetSecurity, int userId) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
if (mActivityMonitors != null) {
synchronized (mSync) {
@@ -1904,7 +1906,8 @@
.startActivityAsCaller(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
- requestCode, 0, null, options, ignoreTargetSecurity, userId);
+ requestCode, 0, null, options, permissionToken,
+ ignoreTargetSecurity, userId);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
diff --git a/android/app/KeyguardManager.java b/android/app/KeyguardManager.java
index d0f84c8..553099f 100644
--- a/android/app/KeyguardManager.java
+++ b/android/app/KeyguardManager.java
@@ -20,6 +20,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.app.trust.ITrustManager;
import android.content.Context;
@@ -166,24 +167,26 @@
* clicking this button, the activity returns
* {@link #RESULT_ALTERNATE}
*
- * @return the intent for launching the activity or null if the credential of the previous
- * owner can not be verified (e.g. because there was none, or the device does not support
- * verifying credentials after a factory reset, or device setup has already been completed).
- *
+ * @return the intent for launching the activity or null if the previous owner of the device
+ * did not set a credential.
+ * @throws UnsupportedOperationException if the device does not support factory reset
+ * credentials
+ * @throws IllegalStateException if the device has already been provisioned
* @hide
*/
+ @SystemApi
public Intent createConfirmFactoryResetCredentialIntent(
CharSequence title, CharSequence description, CharSequence alternateButtonLabel) {
if (!LockPatternUtils.frpCredentialEnabled(mContext)) {
Log.w(TAG, "Factory reset credentials not supported.");
- return null;
+ throw new UnsupportedOperationException("not supported on this device");
}
// Cannot verify credential if the device is provisioned
if (Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.DEVICE_PROVISIONED, 0) != 0) {
Log.e(TAG, "Factory reset credential cannot be verified after provisioning.");
- return null;
+ throw new IllegalStateException("must not be provisioned yet");
}
// Make sure we have a credential
@@ -192,8 +195,10 @@
ServiceManager.getService(Context.PERSISTENT_DATA_BLOCK_SERVICE));
if (pdb == null) {
Log.e(TAG, "No persistent data block service");
- return null;
+ throw new UnsupportedOperationException("not supported on this device");
}
+ // The following will throw an UnsupportedOperationException if the device does not
+ // support factory reset credentials (or something went wrong retrieving it).
if (!pdb.hasFrpCredentialHandle()) {
Log.i(TAG, "The persistent data block does not have a factory reset credential.");
return null;
@@ -475,6 +480,39 @@
*/
public void requestDismissKeyguard(@NonNull Activity activity,
@Nullable KeyguardDismissCallback callback) {
+ requestDismissKeyguard(activity, null /* message */, callback);
+ }
+
+ /**
+ * If the device is currently locked (see {@link #isKeyguardLocked()}, requests the Keyguard to
+ * be dismissed.
+ * <p>
+ * If the Keyguard is not secure or the device is currently in a trusted state, calling this
+ * method will immediately dismiss the Keyguard without any user interaction.
+ * <p>
+ * If the Keyguard is secure and the device is not in a trusted state, this will bring up the
+ * UI so the user can enter their credentials.
+ * <p>
+ * If the value set for the {@link Activity} attr {@link android.R.attr#turnScreenOn} is true,
+ * the screen will turn on when the keyguard is dismissed.
+ *
+ * @param activity The activity requesting the dismissal. The activity must be either visible
+ * by using {@link LayoutParams#FLAG_SHOW_WHEN_LOCKED} or must be in a state in
+ * which it would be visible if Keyguard would not be hiding it. If that's not
+ * the case, the request will fail immediately and
+ * {@link KeyguardDismissCallback#onDismissError} will be invoked.
+ * @param message A message that will be shown in the keyguard explaining why the user
+ * would want to dismiss it.
+ * @param callback The callback to be called if the request to dismiss Keyguard was successful
+ * or {@code null} if the caller isn't interested in knowing the result. The
+ * callback will not be invoked if the activity was destroyed before the
+ * callback was received.
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.SHOW_KEYGUARD_MESSAGE)
+ @SystemApi
+ public void requestDismissKeyguard(@NonNull Activity activity, @Nullable CharSequence message,
+ @Nullable KeyguardDismissCallback callback) {
try {
mAm.dismissKeyguard(activity.getActivityToken(), new IKeyguardDismissCallback.Stub() {
@Override
@@ -497,9 +535,9 @@
activity.mHandler.post(callback::onDismissCancelled);
}
}
- });
+ }, message);
} catch (RemoteException e) {
- Log.i(TAG, "Failed to dismiss keyguard: " + e);
+ throw e.rethrowFromSystemServer();
}
}
diff --git a/android/app/Notification.java b/android/app/Notification.java
index 85c3be8..d6fddfc 100644
--- a/android/app/Notification.java
+++ b/android/app/Notification.java
@@ -1022,10 +1022,18 @@
/**
* {@link #extras} key: A String array containing the people that this notification relates to,
* each of which was supplied to {@link Builder#addPerson(String)}.
+ *
+ * @deprecated the actual objects are now in {@link #EXTRA_PEOPLE_LIST}
*/
public static final String EXTRA_PEOPLE = "android.people";
/**
+ * {@link #extras} key: An arrayList of {@link Person} objects containing the people that
+ * this notification relates to.
+ */
+ public static final String EXTRA_PEOPLE_LIST = "android.people.list";
+
+ /**
* Allow certain system-generated notifications to appear before the device is provisioned.
* Only available to notifications coming from the android package.
* @hide
@@ -1063,10 +1071,20 @@
* direct replies
* {@link android.app.Notification.MessagingStyle} notification. This extra is a
* {@link CharSequence}
+ *
+ * @deprecated use {@link #EXTRA_MESSAGING_PERSON}
*/
public static final String EXTRA_SELF_DISPLAY_NAME = "android.selfDisplayName";
/**
+ * {@link #extras} key: the person to be displayed for all messages sent by the user including
+ * direct replies
+ * {@link android.app.Notification.MessagingStyle} notification. This extra is a
+ * {@link Person}
+ */
+ public static final String EXTRA_MESSAGING_PERSON = "android.messagingUser";
+
+ /**
* {@link #extras} key: a {@link CharSequence} to be displayed as the title to a conversation
* represented by a {@link android.app.Notification.MessagingStyle}
*/
@@ -1250,10 +1268,67 @@
*/
private static final String EXTRA_DATA_ONLY_INPUTS = "android.extra.DATA_ONLY_INPUTS";
+ /**
+ * {@link }: No semantic action defined.
+ */
+ public static final int SEMANTIC_ACTION_NONE = 0;
+
+ /**
+ * {@code SemanticAction}: Reply to a conversation, chat, group, or wherever replies
+ * may be appropriate.
+ */
+ public static final int SEMANTIC_ACTION_REPLY = 1;
+
+ /**
+ * {@code SemanticAction}: Mark content as read.
+ */
+ public static final int SEMANTIC_ACTION_MARK_AS_READ = 2;
+
+ /**
+ * {@code SemanticAction}: Mark content as unread.
+ */
+ public static final int SEMANTIC_ACTION_MARK_AS_UNREAD = 3;
+
+ /**
+ * {@code SemanticAction}: Delete the content associated with the notification. This
+ * could mean deleting an email, message, etc.
+ */
+ public static final int SEMANTIC_ACTION_DELETE = 4;
+
+ /**
+ * {@code SemanticAction}: Archive the content associated with the notification. This
+ * could mean archiving an email, message, etc.
+ */
+ public static final int SEMANTIC_ACTION_ARCHIVE = 5;
+
+ /**
+ * {@code SemanticAction}: Mute the content associated with the notification. This could
+ * mean silencing a conversation or currently playing media.
+ */
+ public static final int SEMANTIC_ACTION_MUTE = 6;
+
+ /**
+ * {@code SemanticAction}: Unmute the content associated with the notification. This could
+ * mean un-silencing a conversation or currently playing media.
+ */
+ public static final int SEMANTIC_ACTION_UNMUTE = 7;
+
+ /**
+ * {@code SemanticAction}: Mark content with a thumbs up.
+ */
+ public static final int SEMANTIC_ACTION_THUMBS_UP = 8;
+
+ /**
+ * {@code SemanticAction}: Mark content with a thumbs down.
+ */
+ public static final int SEMANTIC_ACTION_THUMBS_DOWN = 9;
+
+
private final Bundle mExtras;
private Icon mIcon;
private final RemoteInput[] mRemoteInputs;
private boolean mAllowGeneratedReplies = true;
+ private final @SemanticAction int mSemanticAction;
/**
* Small icon representing the action.
@@ -1288,6 +1363,7 @@
mExtras = Bundle.setDefusable(in.readBundle(), true);
mRemoteInputs = in.createTypedArray(RemoteInput.CREATOR);
mAllowGeneratedReplies = in.readInt() == 1;
+ mSemanticAction = in.readInt();
}
/**
@@ -1295,12 +1371,14 @@
*/
@Deprecated
public Action(int icon, CharSequence title, PendingIntent intent) {
- this(Icon.createWithResource("", icon), title, intent, new Bundle(), null, true);
+ this(Icon.createWithResource("", icon), title, intent, new Bundle(), null, true,
+ SEMANTIC_ACTION_NONE);
}
/** Keep in sync with {@link Notification.Action.Builder#Builder(Action)}! */
private Action(Icon icon, CharSequence title, PendingIntent intent, Bundle extras,
- RemoteInput[] remoteInputs, boolean allowGeneratedReplies) {
+ RemoteInput[] remoteInputs, boolean allowGeneratedReplies,
+ @SemanticAction int semanticAction) {
this.mIcon = icon;
if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) {
this.icon = icon.getResId();
@@ -1310,6 +1388,7 @@
this.mExtras = extras != null ? extras : new Bundle();
this.mRemoteInputs = remoteInputs;
this.mAllowGeneratedReplies = allowGeneratedReplies;
+ this.mSemanticAction = semanticAction;
}
/**
@@ -1348,6 +1427,15 @@
}
/**
+ * Returns the {@code SemanticAction} associated with this {@link Action}. A
+ * {@code SemanticAction} denotes what an {@link Action}'s {@link PendingIntent} will do
+ * (eg. reply, mark as read, delete, etc).
+ */
+ public @SemanticAction int getSemanticAction() {
+ return mSemanticAction;
+ }
+
+ /**
* Get the list of inputs to be collected from the user that ONLY accept data when this
* action is sent. These remote inputs are guaranteed to return true on a call to
* {@link RemoteInput#isDataOnly}.
@@ -1371,6 +1459,7 @@
private boolean mAllowGeneratedReplies = true;
private final Bundle mExtras;
private ArrayList<RemoteInput> mRemoteInputs;
+ private @SemanticAction int mSemanticAction;
/**
* Construct a new builder for {@link Action} object.
@@ -1390,7 +1479,7 @@
* @param intent the {@link PendingIntent} to fire when users trigger this action
*/
public Builder(Icon icon, CharSequence title, PendingIntent intent) {
- this(icon, title, intent, new Bundle(), null, true);
+ this(icon, title, intent, new Bundle(), null, true, SEMANTIC_ACTION_NONE);
}
/**
@@ -1401,11 +1490,12 @@
public Builder(Action action) {
this(action.getIcon(), action.title, action.actionIntent,
new Bundle(action.mExtras), action.getRemoteInputs(),
- action.getAllowGeneratedReplies());
+ action.getAllowGeneratedReplies(), action.getSemanticAction());
}
private Builder(Icon icon, CharSequence title, PendingIntent intent, Bundle extras,
- RemoteInput[] remoteInputs, boolean allowGeneratedReplies) {
+ RemoteInput[] remoteInputs, boolean allowGeneratedReplies,
+ @SemanticAction int semanticAction) {
mIcon = icon;
mTitle = title;
mIntent = intent;
@@ -1415,6 +1505,7 @@
Collections.addAll(mRemoteInputs, remoteInputs);
}
mAllowGeneratedReplies = allowGeneratedReplies;
+ mSemanticAction = semanticAction;
}
/**
@@ -1470,6 +1561,19 @@
}
/**
+ * Sets the {@code SemanticAction} for this {@link Action}. A
+ * {@code SemanticAction} denotes what an {@link Action}'s
+ * {@link PendingIntent} will do (eg. reply, mark as read, delete, etc).
+ * @param semanticAction a SemanticAction defined within {@link Action} with
+ * {@code SEMANTIC_ACTION_} prefixes
+ * @return this object for method chaining
+ */
+ public Builder setSemanticAction(@SemanticAction int semanticAction) {
+ mSemanticAction = semanticAction;
+ return this;
+ }
+
+ /**
* Apply an extender to this action builder. Extenders may be used to add
* metadata or change options on this builder.
*/
@@ -1510,7 +1614,7 @@
RemoteInput[] textInputsArr = textInputs.isEmpty()
? null : textInputs.toArray(new RemoteInput[textInputs.size()]);
return new Action(mIcon, mTitle, mIntent, mExtras, textInputsArr,
- mAllowGeneratedReplies);
+ mAllowGeneratedReplies, mSemanticAction);
}
}
@@ -1522,12 +1626,15 @@
actionIntent, // safe to alias
mExtras == null ? new Bundle() : new Bundle(mExtras),
getRemoteInputs(),
- getAllowGeneratedReplies());
+ getAllowGeneratedReplies(),
+ getSemanticAction());
}
+
@Override
public int describeContents() {
return 0;
}
+
@Override
public void writeToParcel(Parcel out, int flags) {
final Icon ic = getIcon();
@@ -1547,7 +1654,9 @@
out.writeBundle(mExtras);
out.writeTypedArray(mRemoteInputs, flags);
out.writeInt(mAllowGeneratedReplies ? 1 : 0);
+ out.writeInt(mSemanticAction);
}
+
public static final Parcelable.Creator<Action> CREATOR =
new Parcelable.Creator<Action>() {
public Action createFromParcel(Parcel in) {
@@ -1809,6 +1918,29 @@
return (mFlags & FLAG_HINT_DISPLAY_INLINE) != 0;
}
}
+
+ /**
+ * Provides meaning to an {@link Action} that hints at what the associated
+ * {@link PendingIntent} will do. For example, an {@link Action} with a
+ * {@link PendingIntent} that replies to a text message notification may have the
+ * {@link #SEMANTIC_ACTION_REPLY} {@code SemanticAction} set within it.
+ *
+ * @hide
+ */
+ @IntDef(prefix = { "SEMANTIC_ACTION_" }, value = {
+ SEMANTIC_ACTION_NONE,
+ SEMANTIC_ACTION_REPLY,
+ SEMANTIC_ACTION_MARK_AS_READ,
+ SEMANTIC_ACTION_MARK_AS_UNREAD,
+ SEMANTIC_ACTION_DELETE,
+ SEMANTIC_ACTION_ARCHIVE,
+ SEMANTIC_ACTION_MUTE,
+ SEMANTIC_ACTION_UNMUTE,
+ SEMANTIC_ACTION_THUMBS_UP,
+ SEMANTIC_ACTION_THUMBS_DOWN
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SemanticAction {}
}
/**
@@ -2819,7 +2951,7 @@
private Bundle mUserExtras = new Bundle();
private Style mStyle;
private ArrayList<Action> mActions = new ArrayList<Action>(MAX_ACTION_BUTTONS);
- private ArrayList<String> mPersonList = new ArrayList<String>();
+ private ArrayList<Person> mPersonList = new ArrayList<>();
private NotificationColorUtil mColorUtil;
private boolean mIsLegacy;
private boolean mIsLegacyInitialized;
@@ -2910,8 +3042,9 @@
Collections.addAll(mActions, mN.actions);
}
- if (mN.extras.containsKey(EXTRA_PEOPLE)) {
- Collections.addAll(mPersonList, mN.extras.getStringArray(EXTRA_PEOPLE));
+ if (mN.extras.containsKey(EXTRA_PEOPLE_LIST)) {
+ ArrayList<Person> people = mN.extras.getParcelableArrayList(EXTRA_PEOPLE_LIST);
+ mPersonList.addAll(people);
}
if (mN.getSmallIcon() == null && mN.icon != 0) {
@@ -3621,13 +3754,41 @@
* URIs. The path part of these URIs must exist in the contacts database, in the
* appropriate column, or the reference will be discarded as invalid. Telephone schema
* URIs will be resolved by {@link android.provider.ContactsContract.PhoneLookup}.
+ * It is also possible to provide a URI with the schema {@code name:} in order to uniquely
+ * identify a person without an entry in the contacts database.
* </P>
*
* @param uri A URI for the person.
* @see Notification#EXTRA_PEOPLE
+ * @deprecated use {@link #addPerson(Person)}
*/
public Builder addPerson(String uri) {
- mPersonList.add(uri);
+ addPerson(new Person().setUri(uri));
+ return this;
+ }
+
+ /**
+ * Add a person that is relevant to this notification.
+ *
+ * <P>
+ * Depending on user preferences, this annotation may allow the notification to pass
+ * through interruption filters, if this notification is of category {@link #CATEGORY_CALL}
+ * or {@link #CATEGORY_MESSAGE}. The addition of people may also cause this notification to
+ * appear more prominently in the user interface.
+ * </P>
+ *
+ * <P>
+ * A person should usually contain a uri in order to benefit from the ranking boost.
+ * However, even if no uri is provided, it's beneficial to provide other people in the
+ * notification, such that listeners and voice only devices can announce and handle them
+ * properly.
+ * </P>
+ *
+ * @param person the person to add.
+ * @see Notification#EXTRA_PEOPLE_LIST
+ */
+ public Builder addPerson(Person person) {
+ mPersonList.add(person);
return this;
}
@@ -3934,7 +4095,10 @@
contentView.setViewVisibility(R.id.chronometer, View.GONE);
contentView.setViewVisibility(R.id.header_text, View.GONE);
contentView.setTextViewText(R.id.header_text, null);
+ contentView.setViewVisibility(R.id.header_text_secondary, View.GONE);
+ contentView.setTextViewText(R.id.header_text_secondary, null);
contentView.setViewVisibility(R.id.header_text_divider, View.GONE);
+ contentView.setViewVisibility(R.id.header_text_secondary_divider, View.GONE);
contentView.setViewVisibility(R.id.time_divider, View.GONE);
contentView.setViewVisibility(R.id.time, View.GONE);
contentView.setImageViewIcon(R.id.profile_badge, null);
@@ -3965,8 +4129,8 @@
final Bundle ex = mN.extras;
updateBackgroundColor(contentView);
- bindNotificationHeader(contentView, p.ambient);
- bindLargeIcon(contentView, p.hideLargeIcon, p.alwaysShowReply);
+ bindNotificationHeader(contentView, p.ambient, p.headerTextSecondary);
+ bindLargeIcon(contentView, p.hideLargeIcon || p.ambient, p.alwaysShowReply);
boolean showProgress = handleProgressBar(p.hasProgress, contentView, ex);
if (p.title != null) {
contentView.setViewVisibility(R.id.title, View.VISIBLE);
@@ -4248,12 +4412,14 @@
return null;
}
- private void bindNotificationHeader(RemoteViews contentView, boolean ambient) {
+ private void bindNotificationHeader(RemoteViews contentView, boolean ambient,
+ CharSequence secondaryHeaderText) {
bindSmallIcon(contentView, ambient);
bindHeaderAppName(contentView, ambient);
if (!ambient) {
// Ambient view does not have these
bindHeaderText(contentView);
+ bindHeaderTextSecondary(contentView, secondaryHeaderText);
bindHeaderChronometerAndTime(contentView);
bindProfileBadge(contentView);
}
@@ -4322,6 +4488,17 @@
}
}
+ private void bindHeaderTextSecondary(RemoteViews contentView, CharSequence secondaryText) {
+ if (!TextUtils.isEmpty(secondaryText)) {
+ contentView.setTextViewText(R.id.header_text_secondary, processTextSpans(
+ processLegacyText(secondaryText)));
+ setTextViewColorSecondary(contentView, R.id.header_text_secondary);
+ contentView.setViewVisibility(R.id.header_text_secondary, View.VISIBLE);
+ contentView.setViewVisibility(R.id.header_text_secondary_divider, View.VISIBLE);
+ setTextViewColorSecondary(contentView, R.id.header_text_secondary_divider);
+ }
+ }
+
/**
* @hide
*/
@@ -4555,7 +4732,7 @@
ambient ? R.layout.notification_template_ambient_header
: R.layout.notification_template_header);
resetNotificationHeader(header);
- bindNotificationHeader(header, ambient);
+ bindNotificationHeader(header, ambient, null);
if (colorized != null) {
mN.extras.putBoolean(EXTRA_COLORIZED, colorized);
} else {
@@ -4968,8 +5145,7 @@
mActions.toArray(mN.actions);
}
if (!mPersonList.isEmpty()) {
- mN.extras.putStringArray(EXTRA_PEOPLE,
- mPersonList.toArray(new String[mPersonList.size()]));
+ mN.extras.putParcelableArrayList(EXTRA_PEOPLE_LIST, mPersonList);
}
if (mN.bigContentView != null || mN.contentView != null
|| mN.headsUpContentView != null) {
@@ -5965,7 +6141,7 @@
*/
public static final int MAXIMUM_RETAINED_MESSAGES = 25;
- CharSequence mUserDisplayName;
+ @NonNull Person mUser;
@Nullable CharSequence mConversationTitle;
List<Message> mMessages = new ArrayList<>();
List<Message> mHistoricMessages = new ArrayList<>();
@@ -5979,23 +6155,54 @@
* user before the posting app reposts the notification with those messages after they've
* been actually sent and in previous messages sent by the user added in
* {@link #addMessage(Notification.MessagingStyle.Message)}
+ *
+ * @deprecated use {@code MessagingStyle(Person)}
*/
public MessagingStyle(@NonNull CharSequence userDisplayName) {
- mUserDisplayName = userDisplayName;
+ this(new Person().setName(userDisplayName));
+ }
+
+ /**
+ * @param user Required - The person displayed for any messages that are sent by the
+ * user. Any messages added with {@link #addMessage(Notification.MessagingStyle.Message)}
+ * who don't have a Person associated with it will be displayed as if they were sent
+ * by this user. The user also needs to have a valid name associated with it.
+ */
+ public MessagingStyle(@NonNull Person user) {
+ mUser = user;
+ if (user == null || user.getName() == null) {
+ throw new RuntimeException("user must be valid and have a name");
+ }
+ }
+
+ /**
+ * @return the user to be displayed for any replies sent by the user
+ */
+ public Person getUser() {
+ return mUser;
}
/**
* Returns the name to be displayed for any replies sent by the user
+ *
+ * @deprecated use {@link #getUser()} instead
*/
public CharSequence getUserDisplayName() {
- return mUserDisplayName;
+ return mUser.getName();
}
/**
* Sets the title to be displayed on this conversation. May be set to {@code null}.
*
- * @param conversationTitle A name for the conversation, or {@code null}
- * @return this object for method chaining.
+ * <p>This API's behavior was changed in SDK version {@link Build.VERSION_CODES#P}. If your
+ * application's target version is less than {@link Build.VERSION_CODES#P}, setting a
+ * conversation title to a non-null value will make {@link #isGroupConversation()} return
+ * {@code true} and passing {@code null} will make it return {@code false}. In
+ * {@link Build.VERSION_CODES#P} and beyond, use {@link #setGroupConversation(boolean)}
+ * to set group conversation status.
+ *
+ * @param conversationTitle Title displayed for this conversation
+ * @return this object for method chaining
*/
public MessagingStyle setConversationTitle(@Nullable CharSequence conversationTitle) {
mConversationTitle = conversationTitle;
@@ -6024,8 +6231,28 @@
* @see Message#Message(CharSequence, long, CharSequence)
*
* @return this object for method chaining
+ *
+ * @deprecated use {@link #addMessage(CharSequence, long, Person)}
*/
public MessagingStyle addMessage(CharSequence text, long timestamp, CharSequence sender) {
+ return addMessage(text, timestamp,
+ sender == null ? null : new Person().setName(sender));
+ }
+
+ /**
+ * Adds a message for display by this notification. Convenience call for a simple
+ * {@link Message} in {@link #addMessage(Notification.MessagingStyle.Message)}.
+ * @param text A {@link CharSequence} to be displayed as the message content
+ * @param timestamp Time at which the message arrived
+ * @param sender The {@link Person} who sent the message.
+ * Should be <code>null</code> for messages by the current user, in which case
+ * the platform will insert the user set in {@code MessagingStyle(Person)}.
+ *
+ * @see Message#Message(CharSequence, long, CharSequence)
+ *
+ * @return this object for method chaining
+ */
+ public MessagingStyle addMessage(CharSequence text, long timestamp, Person sender) {
return addMessage(new Message(text, timestamp, sender));
}
@@ -6083,6 +6310,7 @@
/**
* Sets whether this conversation notification represents a group.
+ *
* @param isGroupConversation {@code true} if the conversation represents a group,
* {@code false} otherwise.
* @return this object for method chaining
@@ -6093,9 +6321,27 @@
}
/**
- * Returns {@code true} if this notification represents a group conversation.
+ * Returns {@code true} if this notification represents a group conversation, otherwise
+ * {@code false}.
+ *
+ * <p> If the application that generated this {@link MessagingStyle} targets an SDK version
+ * less than {@link Build.VERSION_CODES#P}, this method becomes dependent on whether or
+ * not the conversation title is set; returning {@code true} if the conversation title is
+ * a non-null value, or {@code false} otherwise. From {@link Build.VERSION_CODES#P} forward,
+ * this method returns what's set by {@link #setGroupConversation(boolean)} allowing for
+ * named, non-group conversations.
+ *
+ * @see #setConversationTitle(CharSequence)
*/
public boolean isGroupConversation() {
+ // When target SDK version is < P, a non-null conversation title dictates if this is
+ // as group conversation.
+ if (mBuilder != null
+ && mBuilder.mContext.getApplicationInfo().targetSdkVersion
+ < Build.VERSION_CODES.P) {
+ return mConversationTitle != null;
+ }
+
return mIsGroupConversation;
}
@@ -6105,8 +6351,10 @@
@Override
public void addExtras(Bundle extras) {
super.addExtras(extras);
- if (mUserDisplayName != null) {
- extras.putCharSequence(EXTRA_SELF_DISPLAY_NAME, mUserDisplayName);
+ if (mUser != null) {
+ // For legacy usages
+ extras.putCharSequence(EXTRA_SELF_DISPLAY_NAME, mUser.getName());
+ extras.putParcelable(EXTRA_MESSAGING_PERSON, mUser);
}
if (mConversationTitle != null) {
extras.putCharSequence(EXTRA_CONVERSATION_TITLE, mConversationTitle);
@@ -6126,14 +6374,15 @@
Message m = findLatestIncomingMessage();
CharSequence text = (m == null) ? null : m.mText;
CharSequence sender = m == null ? null
- : TextUtils.isEmpty(m.mSender) ? mUserDisplayName : m.mSender;
+ : m.mSender == null || TextUtils.isEmpty(m.mSender.getName())
+ ? mUser.getName() : m.mSender.getName();
CharSequence title;
if (!TextUtils.isEmpty(mConversationTitle)) {
if (!TextUtils.isEmpty(sender)) {
BidiFormatter bidi = BidiFormatter.getInstance();
title = mBuilder.mContext.getString(
com.android.internal.R.string.notification_messaging_title_template,
- bidi.unicodeWrap(mConversationTitle), bidi.unicodeWrap(m.mSender));
+ bidi.unicodeWrap(mConversationTitle), bidi.unicodeWrap(sender));
} else {
title = mConversationTitle;
}
@@ -6156,7 +6405,11 @@
protected void restoreFromExtras(Bundle extras) {
super.restoreFromExtras(extras);
- mUserDisplayName = extras.getCharSequence(EXTRA_SELF_DISPLAY_NAME);
+ mUser = extras.getParcelable(EXTRA_MESSAGING_PERSON);
+ if (mUser == null) {
+ CharSequence displayName = extras.getCharSequence(EXTRA_SELF_DISPLAY_NAME);
+ mUser = new Person().setName(displayName);
+ }
mConversationTitle = extras.getCharSequence(EXTRA_CONVERSATION_TITLE);
Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES);
mMessages = Message.getMessagesFromBundleArray(messages);
@@ -6172,7 +6425,7 @@
public RemoteViews makeContentView(boolean increasedHeight) {
mBuilder.mOriginalActions = mBuilder.mActions;
mBuilder.mActions = new ArrayList<>();
- RemoteViews remoteViews = makeBigContentView();
+ RemoteViews remoteViews = makeBigContentView(true /* showRightIcon */);
mBuilder.mActions = mBuilder.mOriginalActions;
mBuilder.mOriginalActions = null;
return remoteViews;
@@ -6191,7 +6444,7 @@
for (int i = messages.size() - 1; i >= 0; i--) {
Message m = messages.get(i);
// Incoming messages have a non-empty sender.
- if (!TextUtils.isEmpty(m.mSender)) {
+ if (m.mSender != null && !TextUtils.isEmpty(m.mSender.getName())) {
return m;
}
}
@@ -6207,27 +6460,40 @@
*/
@Override
public RemoteViews makeBigContentView() {
+ return makeBigContentView(false /* showRightIcon */);
+ }
+
+ @NonNull
+ private RemoteViews makeBigContentView(boolean showRightIcon) {
CharSequence conversationTitle = !TextUtils.isEmpty(super.mBigContentTitle)
? super.mBigContentTitle
: mConversationTitle;
boolean isOneToOne = TextUtils.isEmpty(conversationTitle);
- if (isOneToOne) {
- // Let's add the conversationTitle in case we didn't have one before and all
- // messages are from the same sender
- conversationTitle = createConversationTitleFromMessages();
- } else if (hasOnlyWhiteSpaceSenders()) {
+ CharSequence nameReplacement = null;
+ if (hasOnlyWhiteSpaceSenders()) {
isOneToOne = true;
+ nameReplacement = conversationTitle;
+ conversationTitle = null;
}
- boolean hasTitle = !TextUtils.isEmpty(conversationTitle);
RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(
mBuilder.getMessagingLayoutResource(),
mBuilder.mParams.reset().hasProgress(false).title(conversationTitle).text(null)
- .hideLargeIcon(isOneToOne).alwaysShowReply(true));
+ .hideLargeIcon(!showRightIcon || isOneToOne)
+ .headerTextSecondary(conversationTitle)
+ .alwaysShowReply(showRightIcon));
addExtras(mBuilder.mN.extras);
+ // also update the end margin if there is an image
+ int endMargin = R.dimen.notification_content_margin_end;
+ if (mBuilder.mN.hasLargeIcon() && showRightIcon) {
+ endMargin = R.dimen.notification_content_plus_picture_margin_end;
+ }
+ contentView.setViewLayoutMarginEndDimen(R.id.notification_main_column, endMargin);
contentView.setInt(R.id.status_bar_latest_event_content, "setLayoutColor",
mBuilder.resolveContrastColor());
contentView.setIcon(R.id.status_bar_latest_event_content, "setLargeIcon",
mBuilder.mN.mLargeIcon);
+ contentView.setCharSequence(R.id.status_bar_latest_event_content, "setNameReplacement",
+ nameReplacement);
contentView.setBoolean(R.id.status_bar_latest_event_content, "setIsOneToOne",
isOneToOne);
contentView.setBundle(R.id.status_bar_latest_event_content, "setData",
@@ -6238,8 +6504,8 @@
private boolean hasOnlyWhiteSpaceSenders() {
for (int i = 0; i < mMessages.size(); i++) {
Message m = mMessages.get(i);
- CharSequence sender = m.getSender();
- if (!isWhiteSpace(sender)) {
+ Person sender = m.getSenderPerson();
+ if (sender != null && !isWhiteSpace(sender.getName())) {
return false;
}
}
@@ -6268,9 +6534,9 @@
ArraySet<CharSequence> names = new ArraySet<>();
for (int i = 0; i < mMessages.size(); i++) {
Message m = mMessages.get(i);
- CharSequence sender = m.getSender();
+ Person sender = m.getSenderPerson();
if (sender != null) {
- names.add(sender);
+ names.add(sender.getName());
}
}
SpannableStringBuilder title = new SpannableStringBuilder();
@@ -6290,7 +6556,7 @@
*/
@Override
public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
- RemoteViews remoteViews = makeBigContentView();
+ RemoteViews remoteViews = makeBigContentView(true /* showRightIcon */);
remoteViews.setInt(R.id.notification_messaging, "setMaxDisplayedLines", 1);
return remoteViews;
}
@@ -6305,13 +6571,15 @@
static final String KEY_TEXT = "text";
static final String KEY_TIMESTAMP = "time";
static final String KEY_SENDER = "sender";
+ static final String KEY_SENDER_PERSON = "sender_person";
static final String KEY_DATA_MIME_TYPE = "type";
static final String KEY_DATA_URI= "uri";
static final String KEY_EXTRAS_BUNDLE = "extras";
private final CharSequence mText;
private final long mTimestamp;
- private final CharSequence mSender;
+ @Nullable
+ private final Person mSender;
private Bundle mExtras = new Bundle();
private String mDataMimeType;
@@ -6326,8 +6594,28 @@
* the platform will insert {@link MessagingStyle#getUserDisplayName()}.
* Should be unique amongst all individuals in the conversation, and should be
* consistent during re-posts of the notification.
+ *
+ * @deprecated use {@code Message(CharSequence, long, Person)}
*/
public Message(CharSequence text, long timestamp, CharSequence sender){
+ this(text, timestamp, sender == null ? null : new Person().setName(sender));
+ }
+
+ /**
+ * Constructor
+ * @param text A {@link CharSequence} to be displayed as the message content
+ * @param timestamp Time at which the message arrived
+ * @param sender The {@link Person} who sent the message.
+ * Should be <code>null</code> for messages by the current user, in which case
+ * the platform will insert the user set in {@code MessagingStyle(Person)}.
+ * <p>
+ * The person provided should contain an Icon, set with {@link Person#setIcon(Icon)}
+ * and also have a name provided with {@link Person#setName(CharSequence)}. If multiple
+ * users have the same name, consider providing a key with {@link Person#setKey(String)}
+ * in order to differentiate between the different users.
+ * </p>
+ */
+ public Message(CharSequence text, long timestamp, @Nullable Person sender){
mText = text;
mTimestamp = timestamp;
mSender = sender;
@@ -6390,8 +6678,18 @@
/**
* Get the text used to display the contact's name in the messaging experience
+ *
+ * @deprecated use {@link #getSenderPerson()}
*/
public CharSequence getSender() {
+ return mSender == null ? null : mSender.getName();
+ }
+
+ /**
+ * Get the sender associated with this message.
+ */
+ @Nullable
+ public Person getSenderPerson() {
return mSender;
}
@@ -6417,7 +6715,9 @@
}
bundle.putLong(KEY_TIMESTAMP, mTimestamp);
if (mSender != null) {
- bundle.putCharSequence(KEY_SENDER, mSender);
+ // Legacy listeners need this
+ bundle.putCharSequence(KEY_SENDER, mSender.getName());
+ bundle.putParcelable(KEY_SENDER_PERSON, mSender);
}
if (mDataMimeType != null) {
bundle.putString(KEY_DATA_MIME_TYPE, mDataMimeType);
@@ -6466,8 +6766,20 @@
if (!bundle.containsKey(KEY_TEXT) || !bundle.containsKey(KEY_TIMESTAMP)) {
return null;
} else {
+
+ Person senderPerson = bundle.getParcelable(KEY_SENDER_PERSON);
+ if (senderPerson == null) {
+ // Legacy apps that use compat don't actually provide the sender objects
+ // We need to fix the compat version to provide people / use
+ // the native api instead
+ CharSequence senderName = bundle.getCharSequence(KEY_SENDER);
+ if (senderName != null) {
+ senderPerson = new Person().setName(senderName);
+ }
+ }
Message message = new Message(bundle.getCharSequence(KEY_TEXT),
- bundle.getLong(KEY_TIMESTAMP), bundle.getCharSequence(KEY_SENDER));
+ bundle.getLong(KEY_TIMESTAMP),
+ senderPerson);
if (bundle.containsKey(KEY_DATA_MIME_TYPE) &&
bundle.containsKey(KEY_DATA_URI)) {
message.setData(bundle.getString(KEY_DATA_MIME_TYPE),
@@ -7102,6 +7414,176 @@
}
}
+ /**
+ * A Person associated with this Notification.
+ */
+ public static final class Person implements Parcelable {
+ @Nullable private CharSequence mName;
+ @Nullable private Icon mIcon;
+ @Nullable private String mUri;
+ @Nullable private String mKey;
+
+ protected Person(Parcel in) {
+ mName = in.readCharSequence();
+ if (in.readInt() != 0) {
+ mIcon = Icon.CREATOR.createFromParcel(in);
+ }
+ mUri = in.readString();
+ mKey = in.readString();
+ }
+
+ /**
+ * Create a new person.
+ */
+ public Person() {
+ }
+
+ /**
+ * Give this person a name.
+ *
+ * @param name the name of this person
+ */
+ public Person setName(@Nullable CharSequence name) {
+ this.mName = name;
+ return this;
+ }
+
+ /**
+ * Add an icon for this person.
+ * <br />
+ * This is currently only used for {@link MessagingStyle} notifications and should not be
+ * provided otherwise, in order to save memory. The system will prefer this icon over any
+ * images that are resolved from the URI.
+ *
+ * @param icon the icon of the person
+ */
+ public Person setIcon(@Nullable Icon icon) {
+ this.mIcon = icon;
+ return this;
+ }
+
+ /**
+ * Set a URI associated with this person.
+ *
+ * <P>
+ * Depending on user preferences, adding a URI to a Person may allow the notification to
+ * pass through interruption filters, if this notification is of
+ * category {@link #CATEGORY_CALL} or {@link #CATEGORY_MESSAGE}.
+ * The addition of people may also cause this notification to appear more prominently in
+ * the user interface.
+ * </P>
+ *
+ * <P>
+ * The person should be specified by the {@code String} representation of a
+ * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}.
+ * </P>
+ *
+ * <P>The system will also attempt to resolve {@code mailto:} and {@code tel:} schema
+ * URIs. The path part of these URIs must exist in the contacts database, in the
+ * appropriate column, or the reference will be discarded as invalid. Telephone schema
+ * URIs will be resolved by {@link android.provider.ContactsContract.PhoneLookup}.
+ * </P>
+ *
+ * @param uri a URI for the person
+ */
+ public Person setUri(@Nullable String uri) {
+ mUri = uri;
+ return this;
+ }
+
+ /**
+ * Add a key to this person in order to uniquely identify it.
+ * This is especially useful if the name doesn't uniquely identify this person or if the
+ * display name is a short handle of the actual name.
+ *
+ * <P>If no key is provided, the name serves as as the key for the purpose of
+ * identification.</P>
+ *
+ * @param key the key that uniquely identifies this person
+ */
+ public Person setKey(@Nullable String key) {
+ mKey = key;
+ return this;
+ }
+
+
+ /**
+ * @return the uri provided for this person or {@code null} if no Uri was provided
+ */
+ @Nullable
+ public String getUri() {
+ return mUri;
+ }
+
+ /**
+ * @return the name provided for this person or {@code null} if no name was provided
+ */
+ @Nullable
+ public CharSequence getName() {
+ return mName;
+ }
+
+ /**
+ * @return the icon provided for this person or {@code null} if no icon was provided
+ */
+ @Nullable
+ public Icon getIcon() {
+ return mIcon;
+ }
+
+ /**
+ * @return the key provided for this person or {@code null} if no key was provided
+ */
+ @Nullable
+ public String getKey() {
+ return mKey;
+ }
+
+ /**
+ * @return the URI associated with this person, or "name:mName" otherwise
+ * @hide
+ */
+ public String resolveToLegacyUri() {
+ if (mUri != null) {
+ return mUri;
+ }
+ if (mName != null) {
+ return "name:" + mName;
+ }
+ return "";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, @WriteFlags int flags) {
+ dest.writeCharSequence(mName);
+ if (mIcon != null) {
+ dest.writeInt(1);
+ mIcon.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeString(mUri);
+ dest.writeString(mKey);
+ }
+
+ public static final Creator<Person> CREATOR = new Creator<Person>() {
+ @Override
+ public Person createFromParcel(Parcel in) {
+ return new Person(in);
+ }
+
+ @Override
+ public Person[] newArray(int size) {
+ return new Person[size];
+ }
+ };
+ }
+
// When adding a new Style subclass here, don't forget to update
// Builder.getNotificationStyleClass.
@@ -8541,6 +9023,7 @@
boolean ambient = false;
CharSequence title;
CharSequence text;
+ CharSequence headerTextSecondary;
boolean hideLargeIcon;
public boolean alwaysShowReply;
@@ -8549,6 +9032,7 @@
ambient = false;
title = null;
text = null;
+ headerTextSecondary = null;
return this;
}
@@ -8567,6 +9051,11 @@
return this;
}
+ final StandardTemplateParams headerTextSecondary(CharSequence text) {
+ this.headerTextSecondary = text;
+ return this;
+ }
+
final StandardTemplateParams alwaysShowReply(boolean alwaysShowReply) {
this.alwaysShowReply = alwaysShowReply;
return this;
diff --git a/android/app/NotificationChannel.java b/android/app/NotificationChannel.java
index c06ad3f..30f2697 100644
--- a/android/app/NotificationChannel.java
+++ b/android/app/NotificationChannel.java
@@ -32,8 +32,6 @@
import com.android.internal.util.Preconditions;
-import com.android.internal.util.Preconditions;
-
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
@@ -936,7 +934,9 @@
}
/** @hide */
- public void toProto(ProtoOutputStream proto) {
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
proto.write(NotificationChannelProto.ID, mId);
proto.write(NotificationChannelProto.NAME, mName);
proto.write(NotificationChannelProto.DESCRIPTION, mDesc);
@@ -959,10 +959,10 @@
proto.write(NotificationChannelProto.IS_DELETED, mDeleted);
proto.write(NotificationChannelProto.GROUP, mGroup);
if (mAudioAttributes != null) {
- long aToken = proto.start(NotificationChannelProto.AUDIO_ATTRIBUTES);
- mAudioAttributes.toProto(proto);
- proto.end(aToken);
+ mAudioAttributes.writeToProto(proto, NotificationChannelProto.AUDIO_ATTRIBUTES);
}
proto.write(NotificationChannelProto.IS_BLOCKABLE_SYSTEM, mBlockableSystem);
+
+ proto.end(token);
}
}
diff --git a/android/app/NotificationChannelGroup.java b/android/app/NotificationChannelGroup.java
index 5cb7fb7..16166f7 100644
--- a/android/app/NotificationChannelGroup.java
+++ b/android/app/NotificationChannelGroup.java
@@ -298,13 +298,17 @@
}
/** @hide */
- public void toProto(ProtoOutputStream proto) {
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
proto.write(NotificationChannelGroupProto.ID, mId);
proto.write(NotificationChannelGroupProto.NAME, mName.toString());
proto.write(NotificationChannelGroupProto.DESCRIPTION, mDescription);
proto.write(NotificationChannelGroupProto.IS_BLOCKED, mBlocked);
for (NotificationChannel channel : mChannels) {
- channel.toProto(proto);
+ channel.writeToProto(proto, NotificationChannelGroupProto.CHANNELS);
}
+
+ proto.end(token);
}
}
diff --git a/android/app/NotificationManager.java b/android/app/NotificationManager.java
index 659cf16..49c03ab 100644
--- a/android/app/NotificationManager.java
+++ b/android/app/NotificationManager.java
@@ -93,6 +93,18 @@
private static boolean localLOGV = false;
/**
+ * Intent that is broadcast when an application is blocked or unblocked.
+ *
+ * This broadcast is only sent to the app whose block state has changed.
+ *
+ * Input: nothing
+ * Output: nothing
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_APP_BLOCK_STATE_CHANGED =
+ "android.app.action.APP_BLOCK_STATE_CHANGED";
+
+ /**
* Intent that is broadcast when a {@link NotificationChannel} is blocked
* (when {@link NotificationChannel#getImportance()} is {@link #IMPORTANCE_NONE}) or unblocked
* (when {@link NotificationChannel#getImportance()} is anything other than
@@ -1133,7 +1145,7 @@
}
/** @hide */
- public void toProto(ProtoOutputStream proto, long fieldId) {
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
final long pToken = proto.start(fieldId);
bitwiseToProtoEnum(proto, PolicyProto.PRIORITY_CATEGORIES, priorityCategories);
diff --git a/android/app/PendingIntent.java b/android/app/PendingIntent.java
index 8b76cc7..d6429ae 100644
--- a/android/app/PendingIntent.java
+++ b/android/app/PendingIntent.java
@@ -867,19 +867,30 @@
@Nullable OnFinished onFinished, @Nullable Handler handler,
@Nullable String requiredPermission, @Nullable Bundle options)
throws CanceledException {
+ if (sendAndReturnResult(context, code, intent, onFinished, handler, requiredPermission,
+ options) < 0) {
+ throw new CanceledException();
+ }
+ }
+
+ /**
+ * Like {@link #send}, but returns the result
+ * @hide
+ */
+ public int sendAndReturnResult(Context context, int code, @Nullable Intent intent,
+ @Nullable OnFinished onFinished, @Nullable Handler handler,
+ @Nullable String requiredPermission, @Nullable Bundle options)
+ throws CanceledException {
try {
String resolvedType = intent != null ?
intent.resolveTypeIfNeeded(context.getContentResolver())
: null;
- int res = ActivityManager.getService().sendIntentSender(
+ return ActivityManager.getService().sendIntentSender(
mTarget, mWhitelistToken, code, intent, resolvedType,
onFinished != null
? new FinishedDispatcher(this, onFinished, handler)
: null,
requiredPermission, options);
- if (res < 0) {
- throw new CanceledException();
- }
} catch (RemoteException e) {
throw new CanceledException(e);
}
diff --git a/android/app/ProfilerInfo.java b/android/app/ProfilerInfo.java
index d523427..0ed1b08 100644
--- a/android/app/ProfilerInfo.java
+++ b/android/app/ProfilerInfo.java
@@ -20,6 +20,7 @@
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
import java.io.IOException;
import java.util.Objects;
@@ -55,14 +56,24 @@
*/
public final String agent;
+ /**
+ * Whether the {@link agent} should be attached early (before bind-application) or during
+ * bind-application. Agents attached prior to binding cannot be loaded from the app's APK
+ * directly and must be given as an absolute path (or available in the default LD_LIBRARY_PATH).
+ * Agents attached during bind-application will miss early setup (e.g., resource initialization
+ * and classloader generation), but are searched in the app's library search path.
+ */
+ public final boolean attachAgentDuringBind;
+
public ProfilerInfo(String filename, ParcelFileDescriptor fd, int interval, boolean autoStop,
- boolean streaming, String agent) {
+ boolean streaming, String agent, boolean attachAgentDuringBind) {
profileFile = filename;
profileFd = fd;
samplingInterval = interval;
autoStopProfiler = autoStop;
streamingOutput = streaming;
this.agent = agent;
+ this.attachAgentDuringBind = attachAgentDuringBind;
}
public ProfilerInfo(ProfilerInfo in) {
@@ -72,6 +83,7 @@
autoStopProfiler = in.autoStopProfiler;
streamingOutput = in.streamingOutput;
agent = in.agent;
+ attachAgentDuringBind = in.attachAgentDuringBind;
}
/**
@@ -110,6 +122,21 @@
out.writeInt(autoStopProfiler ? 1 : 0);
out.writeInt(streamingOutput ? 1 : 0);
out.writeString(agent);
+ out.writeBoolean(attachAgentDuringBind);
+ }
+
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(ProfilerInfoProto.PROFILE_FILE, profileFile);
+ if (profileFd != null) {
+ proto.write(ProfilerInfoProto.PROFILE_FD, profileFd.getFd());
+ }
+ proto.write(ProfilerInfoProto.SAMPLING_INTERVAL, samplingInterval);
+ proto.write(ProfilerInfoProto.AUTO_STOP_PROFILER, autoStopProfiler);
+ proto.write(ProfilerInfoProto.STREAMING_OUTPUT, streamingOutput);
+ proto.write(ProfilerInfoProto.AGENT, agent);
+ proto.end(token);
}
public static final Parcelable.Creator<ProfilerInfo> CREATOR =
@@ -132,6 +159,7 @@
autoStopProfiler = in.readInt() != 0;
streamingOutput = in.readInt() != 0;
agent = in.readString();
+ attachAgentDuringBind = in.readBoolean();
}
@Override
diff --git a/android/app/RemoteInput.java b/android/app/RemoteInput.java
index 02a0124..b7100e6 100644
--- a/android/app/RemoteInput.java
+++ b/android/app/RemoteInput.java
@@ -24,6 +24,7 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArraySet;
+
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -73,6 +74,15 @@
private static final String EXTRA_DATA_TYPE_RESULTS_DATA =
"android.remoteinput.dataTypeResultsData";
+ /** Extra added to a clip data intent object identifying the source of the results. */
+ private static final String EXTRA_RESULTS_SOURCE = "android.remoteinput.resultsSource";
+
+ /** The user manually entered the data. */
+ public static final int SOURCE_FREE_FORM_INPUT = 0;
+
+ /** The user selected one of the choices from {@link #getChoices}. */
+ public static final int SOURCE_CHOICE = 1;
+
// Flags bitwise-ored to mFlags
private static final int FLAG_ALLOW_FREE_FORM_INPUT = 0x1;
@@ -416,6 +426,48 @@
intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
}
+ /**
+ * Set the source of the RemoteInput results. This method should only be called by remote
+ * input collection services (e.g.
+ * {@link android.service.notification.NotificationListenerService})
+ * when sending results to a pending intent.
+ *
+ * @see #SOURCE_FREE_FORM_INPUT
+ * @see #SOURCE_CHOICE
+ *
+ * @param intent The intent to add remote input source to. The {@link ClipData}
+ * field of the intent will be modified to contain the source.
+ * field of the intent will be modified to contain the source.
+ * @param source The source of the results.
+ */
+ public static void setResultsSource(Intent intent, int source) {
+ Intent clipDataIntent = getClipDataIntentFromIntent(intent);
+ if (clipDataIntent == null) {
+ clipDataIntent = new Intent(); // First time we've added a result.
+ }
+ clipDataIntent.putExtra(EXTRA_RESULTS_SOURCE, source);
+ intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
+ }
+
+ /**
+ * Get the source of the RemoteInput results.
+ *
+ * @see #SOURCE_FREE_FORM_INPUT
+ * @see #SOURCE_CHOICE
+ *
+ * @param intent The intent object that fired in response to an action or content intent
+ * which also had one or more remote input requested.
+ * @return The source of the results. If no source was set, {@link #SOURCE_FREE_FORM_INPUT} will
+ * be returned.
+ */
+ public static int getResultsSource(Intent intent) {
+ Intent clipDataIntent = getClipDataIntentFromIntent(intent);
+ if (clipDataIntent == null) {
+ return SOURCE_FREE_FORM_INPUT;
+ }
+ return clipDataIntent.getExtras().getInt(EXTRA_RESULTS_SOURCE, SOURCE_FREE_FORM_INPUT);
+ }
+
private static String getExtraResultsKeyForData(String mimeType) {
return EXTRA_DATA_TYPE_RESULTS_DATA + mimeType;
}
diff --git a/android/app/SharedPreferencesImpl.java b/android/app/SharedPreferencesImpl.java
index 6dca400..6ac15a5 100644
--- a/android/app/SharedPreferencesImpl.java
+++ b/android/app/SharedPreferencesImpl.java
@@ -50,11 +50,6 @@
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.FutureTask;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
final class SharedPreferencesImpl implements SharedPreferences {
private static final String TAG = "SharedPreferencesImpl";
@@ -74,12 +69,18 @@
private final Object mLock = new Object();
private final Object mWritingToDiskLock = new Object();
- private Future<Map<String, Object>> mMap;
+ @GuardedBy("mLock")
+ private Map<String, Object> mMap;
+ @GuardedBy("mLock")
+ private Throwable mThrowable;
@GuardedBy("mLock")
private int mDiskWritesInFlight = 0;
@GuardedBy("mLock")
+ private boolean mLoaded = false;
+
+ @GuardedBy("mLock")
private StructTimespec mStatTimestamp;
@GuardedBy("mLock")
@@ -106,18 +107,28 @@
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
+ mLoaded = false;
mMap = null;
+ mThrowable = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
- FutureTask<Map<String, Object>> futureTask = new FutureTask<>(() -> loadFromDisk());
- mMap = futureTask;
- new Thread(futureTask, "SharedPreferencesImpl-load").start();
+ synchronized (mLock) {
+ mLoaded = false;
+ }
+ new Thread("SharedPreferencesImpl-load") {
+ public void run() {
+ loadFromDisk();
+ }
+ }.start();
}
- private Map<String, Object> loadFromDisk() {
+ private void loadFromDisk() {
synchronized (mLock) {
+ if (mLoaded) {
+ return;
+ }
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
@@ -131,13 +142,14 @@
Map<String, Object> map = null;
StructStat stat = null;
+ Throwable thrown = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
- new FileInputStream(mFile), 16*1024);
+ new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
@@ -146,18 +158,37 @@
}
}
} catch (ErrnoException e) {
- /* ignore */
+ // An errno exception means the stat failed. Treat as empty/non-existing by
+ // ignoring.
+ } catch (Throwable t) {
+ thrown = t;
}
synchronized (mLock) {
- if (map != null) {
- mStatTimestamp = stat.st_mtim;
- mStatSize = stat.st_size;
- } else {
- map = new HashMap<>();
+ mLoaded = true;
+ mThrowable = thrown;
+
+ // It's important that we always signal waiters, even if we'll make
+ // them fail with an exception. The try-finally is pretty wide, but
+ // better safe than sorry.
+ try {
+ if (thrown == null) {
+ if (map != null) {
+ mMap = map;
+ mStatTimestamp = stat.st_mtim;
+ mStatSize = stat.st_size;
+ } else {
+ mMap = new HashMap<>();
+ }
+ }
+ // In case of a thrown exception, we retain the old map. That allows
+ // any open editors to commit and store updates.
+ } catch (Throwable t) {
+ mThrowable = t;
+ } finally {
+ mLock.notifyAll();
}
}
- return map;
}
static File makeBackupFile(File prefsFile) {
@@ -216,42 +247,40 @@
}
}
- private @GuardedBy("mLock") Map<String, Object> getLoaded() {
- // For backwards compatibility, we need to ignore any interrupts. b/70122540.
- for (;;) {
- try {
- return mMap.get();
- } catch (ExecutionException e) {
- throw new IllegalStateException(e);
- } catch (InterruptedException e) {
- // Ignore and try again.
- }
- }
- }
- private @GuardedBy("mLock") Map<String, Object> getLoadedWithBlockGuard() {
- if (!mMap.isDone()) {
+ @GuardedBy("mLock")
+ private void awaitLoadedLocked() {
+ if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
- return getLoaded();
+ while (!mLoaded) {
+ try {
+ mLock.wait();
+ } catch (InterruptedException unused) {
+ }
+ }
+ if (mThrowable != null) {
+ throw new IllegalStateException(mThrowable);
+ }
}
@Override
public Map<String, ?> getAll() {
- Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- return new HashMap<String, Object>(map);
+ awaitLoadedLocked();
+ //noinspection unchecked
+ return new HashMap<String, Object>(mMap);
}
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
- Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- String v = (String) map.get(key);
+ awaitLoadedLocked();
+ String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
@@ -259,65 +288,66 @@
@Override
@Nullable
public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
- Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- @SuppressWarnings("unchecked")
- Set<String> v = (Set<String>) map.get(key);
+ awaitLoadedLocked();
+ Set<String> v = (Set<String>) mMap.get(key);
return v != null ? v : defValues;
}
}
@Override
public int getInt(String key, int defValue) {
- Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- Integer v = (Integer) map.get(key);
+ awaitLoadedLocked();
+ Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public long getLong(String key, long defValue) {
- Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- Long v = (Long) map.get(key);
+ awaitLoadedLocked();
+ Long v = (Long)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public float getFloat(String key, float defValue) {
- Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- Float v = (Float) map.get(key);
+ awaitLoadedLocked();
+ Float v = (Float)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public boolean getBoolean(String key, boolean defValue) {
- Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- Boolean v = (Boolean) map.get(key);
+ awaitLoadedLocked();
+ Boolean v = (Boolean)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public boolean contains(String key) {
- Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- return map.containsKey(key);
+ awaitLoadedLocked();
+ return mMap.containsKey(key);
}
}
@Override
public Editor edit() {
- // TODO: remove the need to call getLoaded() when
+ // TODO: remove the need to call awaitLoadedLocked() when
// requesting an editor. will require some work on the
// Editor, but then we should be able to do:
//
// context.getSharedPreferences(..).edit().putString(..).apply()
//
// ... all without blocking.
- getLoadedWithBlockGuard();
+ synchronized (mLock) {
+ awaitLoadedLocked();
+ }
return new EditorImpl();
}
@@ -471,43 +501,13 @@
// a memory commit comes in when we're already
// writing to disk.
if (mDiskWritesInFlight > 0) {
- // We can't modify our map as a currently
+ // We can't modify our mMap as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
- mMap = new Future<Map<String, Object>>() {
- private Map<String, Object> mCopiedMap =
- new HashMap<String, Object>(getLoaded());
-
- @Override
- public boolean cancel(boolean mayInterruptIfRunning) {
- return false;
- }
-
- @Override
- public boolean isCancelled() {
- return false;
- }
-
- @Override
- public boolean isDone() {
- return true;
- }
-
- @Override
- public Map<String, Object> get()
- throws InterruptedException, ExecutionException {
- return mCopiedMap;
- }
-
- @Override
- public Map<String, Object> get(long timeout, TimeUnit unit)
- throws InterruptedException, ExecutionException, TimeoutException {
- return mCopiedMap;
- }
- };
+ mMap = new HashMap<String, Object>(mMap);
}
- mapToWriteToDisk = getLoaded();
+ mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
diff --git a/android/app/StatsManager.java b/android/app/StatsManager.java
new file mode 100644
index 0000000..963fc77
--- /dev/null
+++ b/android/app/StatsManager.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2017 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;
+
+import android.Manifest;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.os.IBinder;
+import android.os.IStatsManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Slog;
+
+/**
+ * API for statsd clients to send configurations and retrieve data.
+ *
+ * @hide
+ */
+@SystemApi
+public final class StatsManager extends android.util.StatsManager { // TODO: Remove the extends.
+ IStatsManager mService;
+ private static final String TAG = "StatsManager";
+
+ /** Long extra of uid that added the relevant stats config. */
+ public static final String EXTRA_STATS_CONFIG_UID =
+ "android.app.extra.STATS_CONFIG_UID";
+ /** Long extra of the relevant stats config's configKey. */
+ public static final String EXTRA_STATS_CONFIG_KEY =
+ "android.app.extra.STATS_CONFIG_KEY";
+ /** Long extra of the relevant statsd_config.proto's Subscription.id. */
+ public static final String EXTRA_STATS_SUBSCRIPTION_ID =
+ "android.app.extra.STATS_SUBSCRIPTION_ID";
+ /** Long extra of the relevant statsd_config.proto's Subscription.rule_id. */
+ public static final String EXTRA_STATS_SUBSCRIPTION_RULE_ID =
+ "android.app.extra.STATS_SUBSCRIPTION_RULE_ID";
+ /**
+ * Extra of a {@link android.os.StatsDimensionsValue} representing sliced dimension value
+ * information.
+ */
+ public static final String EXTRA_STATS_DIMENSIONS_VALUE =
+ "android.app.extra.STATS_DIMENSIONS_VALUE";
+
+ /**
+ * Constructor for StatsManagerClient.
+ *
+ * @hide
+ */
+ public StatsManager() {
+ }
+
+ /**
+ * Clients can send a configuration and simultaneously registers the name of a broadcast
+ * receiver that listens for when it should request data.
+ *
+ * @param configKey An arbitrary integer that allows clients to track the configuration.
+ * @param config Wire-encoded StatsDConfig proto that specifies metrics (and all
+ * dependencies eg, conditions and matchers).
+ * @param pkg The package name to receive the broadcast.
+ * @param cls The name of the class that receives the broadcast.
+ * @return true if successful
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public boolean addConfiguration(long configKey, byte[] config, String pkg, String cls) {
+ synchronized (this) {
+ try {
+ IStatsManager service = getIStatsManagerLocked();
+ if (service == null) {
+ Slog.d(TAG, "Failed to find statsd when adding configuration");
+ return false;
+ }
+ return service.addConfiguration(configKey, config, pkg, cls);
+ } catch (RemoteException e) {
+ Slog.d(TAG, "Failed to connect to statsd when adding configuration");
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Remove a configuration from logging.
+ *
+ * @param configKey Configuration key to remove.
+ * @return true if successful
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public boolean removeConfiguration(long configKey) {
+ synchronized (this) {
+ try {
+ IStatsManager service = getIStatsManagerLocked();
+ if (service == null) {
+ Slog.d(TAG, "Failed to find statsd when removing configuration");
+ return false;
+ }
+ return service.removeConfiguration(configKey);
+ } catch (RemoteException e) {
+ Slog.d(TAG, "Failed to connect to statsd when removing configuration");
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Set the PendingIntent to be used when broadcasting subscriber information to the given
+ * subscriberId within the given config.
+ *
+ * <p>
+ * Suppose that the calling uid has added a config with key configKey, and that in this config
+ * it is specified that when a particular anomaly is detected, a broadcast should be sent to
+ * a BroadcastSubscriber with id subscriberId. This function links the given pendingIntent with
+ * that subscriberId (for that config), so that this pendingIntent is used to send the broadcast
+ * when the anomaly is detected.
+ *
+ * <p>
+ * When statsd sends the broadcast, the PendingIntent will used to send an intent with
+ * information of
+ * {@link #EXTRA_STATS_CONFIG_UID},
+ * {@link #EXTRA_STATS_CONFIG_KEY},
+ * {@link #EXTRA_STATS_SUBSCRIPTION_ID},
+ * {@link #EXTRA_STATS_SUBSCRIPTION_RULE_ID}, and
+ * {@link #EXTRA_STATS_DIMENSIONS_VALUE}.
+ *
+ * <p>
+ * This function can only be called by the owner (uid) of the config. It must be called each
+ * time statsd starts. The config must have been added first (via addConfiguration()).
+ *
+ * @param configKey The integer naming the config to which this subscriber is attached.
+ * @param subscriberId ID of the subscriber, as used in the config.
+ * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
+ * associated with the given subscriberId. May be null, in which case
+ * it undoes any previous setting of this subscriberId.
+ * @return true if successful
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public boolean setBroadcastSubscriber(long configKey,
+ long subscriberId,
+ PendingIntent pendingIntent) {
+ synchronized (this) {
+ try {
+ IStatsManager service = getIStatsManagerLocked();
+ if (service == null) {
+ Slog.w(TAG, "Failed to find statsd when adding broadcast subscriber");
+ return false;
+ }
+ if (pendingIntent != null) {
+ // Extracts IIntentSender from the PendingIntent and turns it into an IBinder.
+ IBinder intentSender = pendingIntent.getTarget().asBinder();
+ return service.setBroadcastSubscriber(configKey, subscriberId, intentSender);
+ } else {
+ return service.unsetBroadcastSubscriber(configKey, subscriberId);
+ }
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to connect to statsd when adding broadcast subscriber", e);
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Clients can request data with a binder call. This getter is destructive and also clears
+ * the retrieved metrics from statsd memory.
+ *
+ * @param configKey Configuration key to retrieve data from.
+ * @return Serialized ConfigMetricsReportList proto. Returns null on failure.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public byte[] getData(long configKey) {
+ synchronized (this) {
+ try {
+ IStatsManager service = getIStatsManagerLocked();
+ if (service == null) {
+ Slog.d(TAG, "Failed to find statsd when getting data");
+ return null;
+ }
+ return service.getData(configKey);
+ } catch (RemoteException e) {
+ Slog.d(TAG, "Failed to connecto statsd when getting data");
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Clients can request metadata for statsd. Will contain stats across all configurations but not
+ * the actual metrics themselves (metrics must be collected via {@link #getData(String)}.
+ * This getter is not destructive and will not reset any metrics/counters.
+ *
+ * @return Serialized StatsdStatsReport proto. Returns null on failure.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public byte[] getMetadata() {
+ synchronized (this) {
+ try {
+ IStatsManager service = getIStatsManagerLocked();
+ if (service == null) {
+ Slog.d(TAG, "Failed to find statsd when getting metadata");
+ return null;
+ }
+ return service.getMetadata();
+ } catch (RemoteException e) {
+ Slog.d(TAG, "Failed to connecto statsd when getting metadata");
+ return null;
+ }
+ }
+ }
+
+ private class StatsdDeathRecipient implements IBinder.DeathRecipient {
+ @Override
+ public void binderDied() {
+ synchronized (this) {
+ mService = null;
+ }
+ }
+ }
+
+ private IStatsManager getIStatsManagerLocked() throws RemoteException {
+ if (mService != null) {
+ return mService;
+ }
+ mService = IStatsManager.Stub.asInterface(ServiceManager.getService("stats"));
+ if (mService != null) {
+ mService.asBinder().linkToDeath(new StatsdDeathRecipient(), 0);
+ }
+ return mService;
+ }
+}
diff --git a/android/app/SystemServiceRegistry.java b/android/app/SystemServiceRegistry.java
index 66cf991..4310434 100644
--- a/android/app/SystemServiceRegistry.java
+++ b/android/app/SystemServiceRegistry.java
@@ -38,12 +38,12 @@
import android.content.Context;
import android.content.IRestrictionsManager;
import android.content.RestrictionsManager;
+import android.content.pm.CrossProfileApps;
+import android.content.pm.ICrossProfileApps;
import android.content.pm.IShortcutService;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutManager;
-import android.content.pm.crossprofile.CrossProfileApps;
-import android.content.pm.crossprofile.ICrossProfileApps;
import android.content.res.Resources;
import android.hardware.ConsumerIrManager;
import android.hardware.ISerialManager;
@@ -112,6 +112,7 @@
import android.os.IHardwarePropertiesManager;
import android.os.IPowerManager;
import android.os.IRecoverySystem;
+import android.os.ISystemUpdateManager;
import android.os.IUserManager;
import android.os.IncidentManager;
import android.os.PowerManager;
@@ -119,6 +120,7 @@
import android.os.RecoverySystem;
import android.os.ServiceManager;
import android.os.ServiceManager.ServiceNotFoundException;
+import android.os.SystemUpdateManager;
import android.os.SystemVibrator;
import android.os.UserHandle;
import android.os.UserManager;
@@ -136,9 +138,9 @@
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
+import android.telephony.euicc.EuiccCardManager;
import android.telephony.euicc.EuiccManager;
import android.util.Log;
-import android.util.StatsManager;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.WindowManager;
@@ -484,6 +486,17 @@
return new StorageStatsManager(ctx, service);
}});
+ registerService(Context.SYSTEM_UPDATE_SERVICE, SystemUpdateManager.class,
+ new CachedServiceFetcher<SystemUpdateManager>() {
+ @Override
+ public SystemUpdateManager createService(ContextImpl ctx)
+ throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(
+ Context.SYSTEM_UPDATE_SERVICE);
+ ISystemUpdateManager service = ISystemUpdateManager.Stub.asInterface(b);
+ return new SystemUpdateManager(service);
+ }});
+
registerService(Context.TELEPHONY_SERVICE, TelephonyManager.class,
new CachedServiceFetcher<TelephonyManager>() {
@Override
@@ -494,7 +507,7 @@
registerService(Context.TELEPHONY_SUBSCRIPTION_SERVICE, SubscriptionManager.class,
new CachedServiceFetcher<SubscriptionManager>() {
@Override
- public SubscriptionManager createService(ContextImpl ctx) {
+ public SubscriptionManager createService(ContextImpl ctx) throws ServiceNotFoundException {
return new SubscriptionManager(ctx.getOuterContext());
}});
@@ -519,6 +532,13 @@
return new EuiccManager(ctx.getOuterContext());
}});
+ registerService(Context.EUICC_CARD_SERVICE, EuiccCardManager.class,
+ new CachedServiceFetcher<EuiccCardManager>() {
+ @Override
+ public EuiccCardManager createService(ContextImpl ctx) {
+ return new EuiccCardManager(ctx.getOuterContext());
+ }});
+
registerService(Context.UI_MODE_SERVICE, UiModeManager.class,
new CachedServiceFetcher<UiModeManager>() {
@Override
diff --git a/android/app/UiAutomation.java b/android/app/UiAutomation.java
index 8f01685..ba39740 100644
--- a/android/app/UiAutomation.java
+++ b/android/app/UiAutomation.java
@@ -24,7 +24,6 @@
import android.annotation.NonNull;
import android.annotation.TestApi;
import android.graphics.Bitmap;
-import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
@@ -47,10 +46,14 @@
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
import android.view.accessibility.IAccessibilityInteractionConnection;
+
+import com.android.internal.util.CollectionUtils;
+
import libcore.io.IoUtils;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeoutException;
@@ -580,6 +583,8 @@
// Execute the command *without* the lock being held.
command.run();
+ List<AccessibilityEvent> eventsReceived = Collections.emptyList();
+
// Acquire the lock and wait for the event.
try {
// Wait for the event.
@@ -600,14 +605,14 @@
if (filter.accept(event)) {
return event;
}
- event.recycle();
+ eventsReceived = CollectionUtils.add(eventsReceived, event);
}
// Check if timed out and if not wait.
final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
if (remainingTimeMillis <= 0) {
throw new TimeoutException("Expected event not received within: "
- + timeoutMillis + " ms.");
+ + timeoutMillis + " ms, among " + eventsReceived);
}
synchronized (mLock) {
if (mEventQueue.isEmpty()) {
@@ -620,6 +625,10 @@
}
}
} finally {
+ for (int i = 0; i < CollectionUtils.size(eventsReceived); i++) {
+ AccessibilityEvent event = eventsReceived.get(i);
+ event.recycle();
+ }
synchronized (mLock) {
mWaitingForEventDelivery = false;
mEventQueue.clear();
diff --git a/android/app/admin/ConnectEvent.java b/android/app/admin/ConnectEvent.java
index f06a925..d511c57 100644
--- a/android/app/admin/ConnectEvent.java
+++ b/android/app/admin/ConnectEvent.java
@@ -68,7 +68,7 @@
@Override
public String toString() {
- return String.format("ConnectEvent(%s, %d, %d, %s)", mIpAddress, mPort, mTimestamp,
+ return String.format("ConnectEvent(%d, %s, %d, %d, %s)", mId, mIpAddress, mPort, mTimestamp,
mPackageName);
}
diff --git a/android/app/admin/DeviceAdminReceiver.java b/android/app/admin/DeviceAdminReceiver.java
index 2e697ac..28e845a 100644
--- a/android/app/admin/DeviceAdminReceiver.java
+++ b/android/app/admin/DeviceAdminReceiver.java
@@ -29,10 +29,14 @@
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
+import android.os.PersistableBundle;
import android.os.Process;
import android.os.UserHandle;
import android.security.KeyChain;
+import libcore.util.NonNull;
+import libcore.util.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -335,7 +339,7 @@
/**
* Broadcast action: notify the device owner that a user or profile has been removed.
* Carries an extra {@link Intent#EXTRA_USER} that has the {@link UserHandle} of
- * the new user.
+ * the user.
* @hide
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
@@ -343,6 +347,36 @@
public static final String ACTION_USER_REMOVED = "android.app.action.USER_REMOVED";
/**
+ * Broadcast action: notify the device owner that a user or profile has been started.
+ * Carries an extra {@link Intent#EXTRA_USER} that has the {@link UserHandle} of
+ * the user.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_USER_STARTED = "android.app.action.USER_STARTED";
+
+ /**
+ * Broadcast action: notify the device owner that a user or profile has been stopped.
+ * Carries an extra {@link Intent#EXTRA_USER} that has the {@link UserHandle} of
+ * the user.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_USER_STOPPED = "android.app.action.USER_STOPPED";
+
+ /**
+ * Broadcast action: notify the device owner that a user or profile has been switched to.
+ * Carries an extra {@link Intent#EXTRA_USER} that has the {@link UserHandle} of
+ * the user.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_USER_SWITCHED = "android.app.action.USER_SWITCHED";
+
+ /**
* A string containing the SHA-256 hash of the bugreport file.
*
* @see #ACTION_BUGREPORT_SHARE
@@ -438,6 +472,65 @@
// TO DO: describe syntax.
public static final String DEVICE_ADMIN_META_DATA = "android.app.device_admin";
+ /**
+ * Broadcast action: notify the newly transferred administrator that the transfer
+ * from the original administrator was successful.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_TRANSFER_OWNERSHIP_COMPLETE =
+ "android.app.action.TRANSFER_OWNERSHIP_COMPLETE";
+
+ /**
+ * Broadcast action: notify the device owner that the ownership of one of its affiliated
+ * profiles is transferred.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_AFFILIATED_PROFILE_TRANSFER_OWNERSHIP_COMPLETE =
+ "android.app.action.AFFILIATED_PROFILE_TRANSFER_OWNERSHIP_COMPLETE";
+
+ /**
+ * A {@link android.os.Parcelable} extra of type {@link android.os.PersistableBundle} that
+ * allows a mobile device management application to pass data to the management application
+ * instance after owner transfer.
+ *
+ * <p>If the transfer is successful, the new owner receives the data in
+ * {@link DeviceAdminReceiver#onTransferOwnershipComplete(Context, PersistableBundle)}.
+ * The bundle is not changed during the ownership transfer.
+ *
+ * @see DevicePolicyManager#transferOwnership(ComponentName, ComponentName, PersistableBundle)
+ */
+ public static final String EXTRA_TRANSFER_OWNERSHIP_ADMIN_EXTRAS_BUNDLE =
+ "android.app.extra.TRANSFER_OWNERSHIP_ADMIN_EXTRAS_BUNDLE";
+
+ /**
+ * Name under which a device administration component indicates whether it supports transfer of
+ * ownership. This meta-data is of type <code>boolean</code>. A value of <code>true</code>
+ * allows this administrator to be used as a target administrator for a transfer. If the value
+ * is <code>false</code>, ownership cannot be transferred to this administrator. The default
+ * value is <code>false</code>.
+ * <p>This metadata is used to avoid ownership transfer migration to an administrator with a
+ * version which does not yet support it.
+ * <p>Usage:
+ * <pre>
+ * <receiver name="..." android:permission="android.permission.BIND_DEVICE_ADMIN">
+ * <meta-data
+ * android:name="android.app.device_admin"
+ * android:resource="@xml/..." />
+ * <meta-data
+ * android:name="android.app.support_transfer_ownership"
+ * android:value="true" />
+ * </receiver>
+ * </pre>
+ *
+ * @see DevicePolicyManager#transferOwnership(ComponentName, ComponentName, PersistableBundle)
+ */
+ public static final String SUPPORT_TRANSFER_OWNERSHIP_META_DATA =
+ "android.app.support_transfer_ownership";
+
private DevicePolicyManager mManager;
private ComponentName mWho;
@@ -860,6 +953,76 @@
}
/**
+ * Called when a user or profile is started.
+ *
+ * <p>This callback is only applicable to device owners.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param startedUser The {@link UserHandle} of the user that has just been started.
+ */
+ public void onUserStarted(Context context, Intent intent, UserHandle startedUser) {
+ }
+
+ /**
+ * Called when a user or profile is stopped.
+ *
+ * <p>This callback is only applicable to device owners.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param stoppedUser The {@link UserHandle} of the user that has just been stopped.
+ */
+ public void onUserStopped(Context context, Intent intent, UserHandle stoppedUser) {
+ }
+
+ /**
+ * Called when a user or profile is switched to.
+ *
+ * <p>This callback is only applicable to device owners.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param switchedUser The {@link UserHandle} of the user that has just been switched to.
+ */
+ public void onUserSwitched(Context context, Intent intent, UserHandle switchedUser) {
+ }
+
+ /**
+ * Called on the newly assigned owner (either device owner or profile owner) when the ownership
+ * transfer has completed successfully.
+ *
+ * <p> The {@code bundle} parameter allows the original owner to pass data
+ * to the new one.
+ *
+ * @param context the running context as per {@link #onReceive}
+ * @param bundle the data to be passed to the new owner
+ */
+ public void onTransferOwnershipComplete(@NonNull Context context,
+ @Nullable PersistableBundle bundle) {
+ }
+
+ /**
+ * Called on the device owner when the ownership of one of its affiliated profiles is
+ * transferred.
+ *
+ * <p>This can be used when transferring both device and profile ownership when using
+ * work profile on a fully managed device. The process would look like this:
+ * <ol>
+ * <li>Transfer profile ownership</li>
+ * <li>The device owner gets notified with this callback</li>
+ * <li>Transfer device ownership</li>
+ * <li>Both profile and device ownerships have been transferred</li>
+ * </ol>
+ *
+ * @param context the running context as per {@link #onReceive}
+ * @param user the {@link UserHandle} of the affiliated user
+ * @see DevicePolicyManager#transferOwnership(ComponentName, ComponentName, PersistableBundle)
+ */
+ public void onTransferAffiliatedProfileOwnershipComplete(Context context, UserHandle user) {
+ }
+
+ /**
* Intercept standard device administrator broadcasts. Implementations
* should not override this method; it is better to implement the
* convenience callbacks for each action.
@@ -921,6 +1084,19 @@
onUserAdded(context, intent, intent.getParcelableExtra(Intent.EXTRA_USER));
} else if (ACTION_USER_REMOVED.equals(action)) {
onUserRemoved(context, intent, intent.getParcelableExtra(Intent.EXTRA_USER));
+ } else if (ACTION_USER_STARTED.equals(action)) {
+ onUserStarted(context, intent, intent.getParcelableExtra(Intent.EXTRA_USER));
+ } else if (ACTION_USER_STOPPED.equals(action)) {
+ onUserStopped(context, intent, intent.getParcelableExtra(Intent.EXTRA_USER));
+ } else if (ACTION_USER_SWITCHED.equals(action)) {
+ onUserSwitched(context, intent, intent.getParcelableExtra(Intent.EXTRA_USER));
+ } else if (ACTION_TRANSFER_OWNERSHIP_COMPLETE.equals(action)) {
+ PersistableBundle bundle =
+ intent.getParcelableExtra(EXTRA_TRANSFER_OWNERSHIP_ADMIN_EXTRAS_BUNDLE);
+ onTransferOwnershipComplete(context, bundle);
+ } else if (ACTION_AFFILIATED_PROFILE_TRANSFER_OWNERSHIP_COMPLETE.equals(action)) {
+ onTransferAffiliatedProfileOwnershipComplete(context,
+ intent.getParcelableExtra(Intent.EXTRA_USER));
}
}
}
diff --git a/android/app/admin/DevicePolicyManager.java b/android/app/admin/DevicePolicyManager.java
index 7e80ac7..8f76032 100644
--- a/android/app/admin/DevicePolicyManager.java
+++ b/android/app/admin/DevicePolicyManager.java
@@ -18,7 +18,6 @@
import android.annotation.CallbackExecutor;
import android.annotation.ColorInt;
-import android.annotation.Condemned;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -50,8 +49,6 @@
import android.net.ProxyInfo;
import android.net.Uri;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerExecutor;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.os.Process;
@@ -71,6 +68,7 @@
import android.security.keystore.ParcelableKeyGenParameterSpec;
import android.service.restrictions.RestrictionsReceiver;
import android.telephony.TelephonyManager;
+import android.telephony.data.ApnSetting;
import android.util.ArraySet;
import android.util.Log;
@@ -1124,6 +1122,7 @@
*
* This broadcast is sent only to the primary user.
* @see #ACTION_PROVISION_MANAGED_DEVICE
+ * @see DevicePolicyManager#transferOwnership(ComponentName, ComponentName, PersistableBundle)
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
public static final String ACTION_DEVICE_OWNER_CHANGED
@@ -1159,9 +1158,17 @@
public static final String POLICY_DISABLE_SCREEN_CAPTURE = "policy_disable_screen_capture";
/**
+ * Constant to indicate the feature of mandatory backups. Used as argument to
+ * {@link #createAdminSupportIntent(String)}.
+ * @see #setMandatoryBackupTransport(ComponentName, ComponentName)
+ */
+ public static final String POLICY_MANDATORY_BACKUPS = "policy_mandatory_backups";
+
+ /**
* A String indicating a specific restricted feature. Can be a user restriction from the
* {@link UserManager}, e.g. {@link UserManager#DISALLOW_ADJUST_VOLUME}, or one of the values
- * {@link #POLICY_DISABLE_CAMERA} or {@link #POLICY_DISABLE_SCREEN_CAPTURE}.
+ * {@link #POLICY_DISABLE_CAMERA}, {@link #POLICY_DISABLE_SCREEN_CAPTURE} or
+ * {@link #POLICY_MANDATORY_BACKUPS}.
* @see #createAdminSupportIntent(String)
* @hide
*/
@@ -1253,6 +1260,26 @@
= "android.app.action.SYSTEM_UPDATE_POLICY_CHANGED";
/**
+ * Broadcast action to notify ManagedProvisioning that
+ * {@link UserManager#DISALLOW_SHARE_INTO_MANAGED_PROFILE} restriction has changed.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_DATA_SHARING_RESTRICTION_CHANGED =
+ "android.app.action.DATA_SHARING_RESTRICTION_CHANGED";
+
+ /**
+ * Broadcast action from ManagedProvisioning to notify that the latest change to
+ * {@link UserManager#DISALLOW_SHARE_INTO_MANAGED_PROFILE} restriction has been successfully
+ * applied (cross profile intent filters updated). Only usesd for CTS tests.
+ * @hide
+ */
+ @TestApi
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_DATA_SHARING_RESTRICTION_APPLIED =
+ "android.app.action.DATA_SHARING_RESTRICTION_APPLIED";
+
+ /**
* Permission policy to prompt user for new permission requests for runtime permissions.
* Already granted or denied permissions are not affected by this.
*/
@@ -1668,6 +1695,56 @@
public static final String ACTION_DEVICE_ADMIN_SERVICE
= "android.app.action.DEVICE_ADMIN_SERVICE";
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = {"ID_TYPE_"}, value = {
+ ID_TYPE_BASE_INFO,
+ ID_TYPE_SERIAL,
+ ID_TYPE_IMEI,
+ ID_TYPE_MEID
+ })
+ public @interface AttestationIdType {}
+
+ /**
+ * Specifies that the device should attest its manufacturer details. For use with
+ * {@link #generateKeyPair}.
+ *
+ * @see #generateKeyPair
+ */
+ public static final int ID_TYPE_BASE_INFO = 1;
+
+ /**
+ * Specifies that the device should attest its serial number. For use with
+ * {@link #generateKeyPair}.
+ *
+ * @see #generateKeyPair
+ */
+ public static final int ID_TYPE_SERIAL = 2;
+
+ /**
+ * Specifies that the device should attest its IMEI. For use with {@link #generateKeyPair}.
+ *
+ * @see #generateKeyPair
+ */
+ public static final int ID_TYPE_IMEI = 4;
+
+ /**
+ * Specifies that the device should attest its MEID. For use with {@link #generateKeyPair}.
+ *
+ * @see #generateKeyPair
+ */
+ public static final int ID_TYPE_MEID = 8;
+
+ /**
+ * Broadcast action: sent when the profile owner is set, changed or cleared.
+ *
+ * This broadcast is sent only to the user managed by the new profile owner.
+ * @see DevicePolicyManager#transferOwnership(ComponentName, ComponentName, PersistableBundle)
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PROFILE_OWNER_CHANGED =
+ "android.app.action.PROFILE_OWNER_CHANGED";
+
/**
* Return true if the given administrator component is currently active (enabled) in the system.
*
@@ -4106,22 +4183,46 @@
* @param algorithm The key generation algorithm, see {@link java.security.KeyPairGenerator}.
* @param keySpec Specification of the key to generate, see
* {@link java.security.KeyPairGenerator}.
+ * @param idAttestationFlags A bitmask of all the identifiers that should be included in the
+ * attestation record ({@code ID_TYPE_BASE_INFO}, {@code ID_TYPE_SERIAL},
+ * {@code ID_TYPE_IMEI} and {@code ID_TYPE_MEID}), or {@code 0} if no device
+ * identification is required in the attestation record.
+ * Device owner, profile owner and their delegated certificate installer can use
+ * {@link #ID_TYPE_BASE_INFO} to request inclusion of the general device information
+ * including manufacturer, model, brand, device and product in the attestation record.
+ * Only device owner and their delegated certificate installer can use
+ * {@link #ID_TYPE_SERIAL}, {@link #ID_TYPE_IMEI} and {@link #ID_TYPE_MEID} to request
+ * unique device identifiers to be attested.
+ * <p>
+ * If any of {@link #ID_TYPE_SERIAL}, {@link #ID_TYPE_IMEI} and {@link #ID_TYPE_MEID}
+ * is set, it is implicitly assumed that {@link #ID_TYPE_BASE_INFO} is also set.
+ * <p>
+ * If any flag is specified, then an attestation challenge must be included in the
+ * {@code keySpec}.
* @return A non-null {@code AttestedKeyPair} if the key generation succeeded, null otherwise.
* @throws SecurityException if {@code admin} is not {@code null} and not a device or profile
- * owner.
- * @throws IllegalArgumentException if the alias in {@code keySpec} is empty, or if the
+ * owner. If Device ID attestation is requested (using {@link #ID_TYPE_SERIAL},
+ * {@link #ID_TYPE_IMEI} or {@link #ID_TYPE_MEID}), the caller must be the Device Owner
+ * or the Certificate Installer delegate.
+ * @throws IllegalArgumentException if the alias in {@code keySpec} is empty, if the
* algorithm specification in {@code keySpec} is not {@code RSAKeyGenParameterSpec}
- * or {@code ECGenParameterSpec}.
+ * or {@code ECGenParameterSpec}, or if Device ID attestation was requested but the
+ * {@code keySpec} does not contain an attestation challenge.
+ * @see KeyGenParameterSpec.Builder#setAttestationChallenge(byte[])
*/
public AttestedKeyPair generateKeyPair(@Nullable ComponentName admin,
- @NonNull String algorithm, @NonNull KeyGenParameterSpec keySpec) {
+ @NonNull String algorithm, @NonNull KeyGenParameterSpec keySpec,
+ @AttestationIdType int idAttestationFlags) {
throwIfParentInstance("generateKeyPair");
try {
final ParcelableKeyGenParameterSpec parcelableSpec =
new ParcelableKeyGenParameterSpec(keySpec);
KeymasterCertificateChain attestationChain = new KeymasterCertificateChain();
+
+ // Translate ID attestation flags to values used by AttestationUtils
final boolean success = mService.generateKeyPair(
- admin, mContext.getPackageName(), algorithm, parcelableSpec, attestationChain);
+ admin, mContext.getPackageName(), algorithm, parcelableSpec,
+ idAttestationFlags, attestationChain);
if (!success) {
Log.e(TAG, "Error generating key via DevicePolicyManagerService.");
return null;
@@ -5982,6 +6083,13 @@
* Called by a profile owner of a managed profile to remove the cross-profile intent filters
* that go from the managed profile to the parent, or from the parent to the managed profile.
* Only removes those that have been set by the profile owner.
+ * <p>
+ * <em>Note</em>: A list of default cross profile intent filters are set up by the system when
+ * the profile is created, some of them ensure the proper functioning of the profile, while
+ * others enable sharing of data from the parent to the managed profile for user convenience.
+ * These default intent filters are not cleared when this API is called. If the default cross
+ * profile data sharing is not desired, they can be disabled with
+ * {@link UserManager#DISALLOW_SHARE_INTO_MANAGED_PROFILE}.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @throws SecurityException if {@code admin} is not a device or profile owner.
@@ -6404,12 +6512,6 @@
public static final int MAKE_USER_DEMO = 0x0004;
/**
- * Flag used by {@link #createAndManageUser} to specify that the newly created user should be
- * started in the background as part of the user creation.
- */
- public static final int START_USER_IN_BACKGROUND = 0x0008;
-
- /**
* Flag used by {@link #createAndManageUser} to specify that the newly created user should skip
* the disabling of system apps during provisioning.
*/
@@ -6422,7 +6524,6 @@
SKIP_SETUP_WIZARD,
MAKE_USER_EPHEMERAL,
MAKE_USER_DEMO,
- START_USER_IN_BACKGROUND,
LEAVE_ALL_SYSTEM_APPS_ENABLED
})
@Retention(RetentionPolicy.SOURCE)
@@ -6451,7 +6552,8 @@
* IllegalArgumentException is thrown.
* @param adminExtras Extras that will be passed to onEnable of the admin receiver on the new
* user.
- * @param flags {@link #SKIP_SETUP_WIZARD} is supported.
+ * @param flags {@link #SKIP_SETUP_WIZARD}, {@link #MAKE_USER_EPHEMERAL} and
+ * {@link #LEAVE_ALL_SYSTEM_APPS_ENABLED} are supported.
* @see UserHandle
* @return the {@link android.os.UserHandle} object for the created user, or {@code null} if the
* user could not be created.
@@ -6470,8 +6572,8 @@
}
/**
- * Called by a device owner to remove a user and all associated data. The primary user can not
- * be removed.
+ * Called by a device owner to remove a user/profile and all associated data. The primary user
+ * can not be removed.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param userHandle the user to remove.
@@ -6488,14 +6590,14 @@
}
/**
- * Called by a device owner to switch the specified user to the foreground.
- * <p> This cannot be used to switch to a managed profile.
+ * Called by a device owner to switch the specified secondary user to the foreground.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param userHandle the user to switch to; null will switch to primary.
* @return {@code true} if the switch was successful, {@code false} otherwise.
* @throws SecurityException if {@code admin} is not a device owner.
* @see Intent#ACTION_USER_FOREGROUND
+ * @see #getSecondaryUsers(ComponentName)
*/
public boolean switchUser(@NonNull ComponentName admin, @Nullable UserHandle userHandle) {
throwIfParentInstance("switchUser");
@@ -6507,13 +6609,32 @@
}
/**
+ * Called by a device owner to start the specified secondary user in background.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param userHandle the user to be stopped.
+ * @return {@code true} if the user can be started, {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ * @see #getSecondaryUsers(ComponentName)
+ */
+ public boolean startUserInBackground(
+ @NonNull ComponentName admin, @NonNull UserHandle userHandle) {
+ throwIfParentInstance("startUserInBackground");
+ try {
+ return mService.startUserInBackground(admin, userHandle);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Called by a device owner to stop the specified secondary user.
- * <p> This cannot be used to stop the primary user or a managed profile.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param userHandle the user to be stopped.
* @return {@code true} if the user can be stopped, {@code false} otherwise.
* @throws SecurityException if {@code admin} is not a device owner.
+ * @see #getSecondaryUsers(ComponentName)
*/
public boolean stopUser(@NonNull ComponentName admin, @NonNull UserHandle userHandle) {
throwIfParentInstance("stopUser");
@@ -6525,14 +6646,13 @@
}
/**
- * Called by a profile owner that is affiliated with the device to stop the calling user
- * and switch back to primary.
- * <p> This has no effect when called on a managed profile.
+ * Called by a profile owner of secondary user that is affiliated with the device to stop the
+ * calling user and switch back to primary.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @return {@code true} if the exit was successful, {@code false} otherwise.
* @throws SecurityException if {@code admin} is not a profile owner affiliated with the device.
- * @see #isAffiliatedUser
+ * @see #getSecondaryUsers(ComponentName)
*/
public boolean logoutUser(@NonNull ComponentName admin) {
throwIfParentInstance("logoutUser");
@@ -6544,17 +6664,18 @@
}
/**
- * Called by a device owner to list all secondary users on the device, excluding managed
- * profiles.
+ * Called by a device owner to list all secondary users on the device. Managed profiles are not
+ * considered as secondary users.
* <p> Used for various user management APIs, including {@link #switchUser}, {@link #removeUser}
* and {@link #stopUser}.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @return list of other {@link UserHandle}s on the device.
* @throws SecurityException if {@code admin} is not a device owner.
- * @see #switchUser
- * @see #removeUser
- * @see #stopUser
+ * @see #removeUser(ComponentName, UserHandle)
+ * @see #switchUser(ComponentName, UserHandle)
+ * @see #startUserInBackground(ComponentName, UserHandle)
+ * @see #stopUser(ComponentName, UserHandle)
*/
public List<UserHandle> getSecondaryUsers(@NonNull ComponentName admin) {
throwIfParentInstance("getSecondaryUsers");
@@ -6694,7 +6815,8 @@
* @param restriction Indicates for which feature the dialog should be displayed. Can be a
* user restriction from {@link UserManager}, e.g.
* {@link UserManager#DISALLOW_ADJUST_VOLUME}, or one of the constants
- * {@link #POLICY_DISABLE_CAMERA} or {@link #POLICY_DISABLE_SCREEN_CAPTURE}.
+ * {@link #POLICY_DISABLE_CAMERA}, {@link #POLICY_DISABLE_SCREEN_CAPTURE} or
+ * {@link #POLICY_MANDATORY_BACKUPS}.
* @return Intent An intent to be used to start the dialog-activity if the restriction is
* set by an admin, or null if the restriction does not exist or no admin set it.
*/
@@ -6915,14 +7037,14 @@
* task. From {@link android.os.Build.VERSION_CODES#M} removing packages from the lock task
* package list results in locked tasks belonging to those packages to be finished.
* <p>
- * This function can only be called by the device owner or by a profile owner of a user/profile
- * that is affiliated with the device. See {@link #isAffiliatedUser}. Any packages
- * set via this method will be cleared if the user becomes unaffiliated.
+ * This function can only be called by the device owner, a profile owner of an affiliated user
+ * or profile, or the profile owner when no device owner is set. See {@link #isAffiliatedUser}.
+ * Any package set via this method will be cleared if the user becomes unaffiliated.
*
* @param packages The list of packages allowed to enter lock task mode
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
- * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of
- * an affiliated user or profile.
+ * @throws SecurityException if {@code admin} is not the device owner, the profile owner of an
+ * affiliated user or profile, or the profile owner when no device owner is set.
* @see #isAffiliatedUser
* @see Activity#startLockTask()
* @see DeviceAdminReceiver#onLockTaskModeEntering(Context, Intent, String)
@@ -6944,8 +7066,8 @@
/**
* Returns the list of packages allowed to start the lock task mode.
*
- * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of
- * an affiliated user or profile.
+ * @throws SecurityException if {@code admin} is not the device owner, the profile owner of an
+ * affiliated user or profile, or the profile owner when no device owner is set.
* @see #isAffiliatedUser
* @see #setLockTaskPackages
*/
@@ -6985,9 +7107,9 @@
* is in LockTask mode. If this method is not called, none of the features listed here will be
* enabled.
* <p>
- * This function can only be called by the device owner or by a profile owner of a user/profile
- * that is affiliated with the device. See {@link #isAffiliatedUser}. Any features
- * set via this method will be cleared if the user becomes unaffiliated.
+ * This function can only be called by the device owner, a profile owner of an affiliated user
+ * or profile, or the profile owner when no device owner is set. See {@link #isAffiliatedUser}.
+ * Any features set via this method will be cleared if the user becomes unaffiliated.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param flags Bitfield of feature flags:
@@ -6998,9 +7120,10 @@
* {@link #LOCK_TASK_FEATURE_RECENTS},
* {@link #LOCK_TASK_FEATURE_GLOBAL_ACTIONS},
* {@link #LOCK_TASK_FEATURE_KEYGUARD}
- * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of
- * an affiliated user or profile.
+ * @throws SecurityException if {@code admin} is not the device owner, the profile owner of an
+ * affiliated user or profile, or the profile owner when no device owner is set.
* @see #isAffiliatedUser
+ * @throws SecurityException if {@code admin} is not the device owner or the profile owner.
*/
public void setLockTaskFeatures(@NonNull ComponentName admin, @LockTaskFeature int flags) {
throwIfParentInstance("setLockTaskFeatures");
@@ -7018,8 +7141,8 @@
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @return bitfield of flags. See {@link #setLockTaskFeatures(ComponentName, int)} for a list.
- * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of
- * an affiliated user or profile.
+ * @throws SecurityException if {@code admin} is not the device owner, the profile owner of an
+ * affiliated user or profile, or the profile owner when no device owner is set.
* @see #isAffiliatedUser
* @see #setLockTaskFeatures
*/
@@ -7454,7 +7577,8 @@
}
/**
- * Called by a device owner to disable the keyguard altogether.
+ * Called by a device owner or profile owner of secondary users that is affiliated with the
+ * device to disable the keyguard altogether.
* <p>
* Setting the keyguard to disabled has the same effect as choosing "None" as the screen lock
* type. However, this call has no effect if a password, pin or pattern is currently set. If a
@@ -7469,7 +7593,10 @@
* @param disabled {@code true} disables the keyguard, {@code false} reenables it.
* @return {@code false} if attempting to disable the keyguard while a lock password was in
* place. {@code true} otherwise.
- * @throws SecurityException if {@code admin} is not a device owner.
+ * @throws SecurityException if {@code admin} is not the device owner, or a profile owner of
+ * secondary user that is affiliated with the device.
+ * @see #isAffiliatedUser
+ * @see #getSecondaryUsers
*/
public boolean setKeyguardDisabled(@NonNull ComponentName admin, boolean disabled) {
throwIfParentInstance("setKeyguardDisabled");
@@ -7481,9 +7608,9 @@
}
/**
- * Called by device owner to disable the status bar. Disabling the status bar blocks
- * notifications, quick settings and other screen overlays that allow escaping from a single use
- * device.
+ * Called by device owner or profile owner of secondary users that is affiliated with the
+ * device to disable the status bar. Disabling the status bar blocks notifications, quick
+ * settings and other screen overlays that allow escaping from a single use device.
* <p>
* <strong>Note:</strong> This method has no effect for LockTask mode. The behavior of the
* status bar in LockTask mode can be configured with
@@ -7494,7 +7621,10 @@
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param disabled {@code true} disables the status bar, {@code false} reenables it.
* @return {@code false} if attempting to disable the status bar failed. {@code true} otherwise.
- * @throws SecurityException if {@code admin} is not a device owner.
+ * @throws SecurityException if {@code admin} is not the device owner, or a profile owner of
+ * secondary user that is affiliated with the device.
+ * @see #isAffiliatedUser
+ * @see #getSecondaryUsers
*/
public boolean setStatusBarDisabled(@NonNull ComponentName admin, boolean disabled) {
throwIfParentInstance("setStatusBarDisabled");
@@ -8100,6 +8230,47 @@
}
/**
+ * Called by a device or profile owner to restrict packages from accessing metered data.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+ * @param packageNames the list of package names to be restricted.
+ * @return a list of package names which could not be restricted.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public @NonNull List<String> setMeteredDataDisabled(@NonNull ComponentName admin,
+ @NonNull List<String> packageNames) {
+ throwIfParentInstance("setMeteredDataDisabled");
+ if (mService != null) {
+ try {
+ return mService.setMeteredDataDisabled(admin, packageNames);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return packageNames;
+ }
+
+ /**
+ * Called by a device or profile owner to retrieve the list of packages which are restricted
+ * by the admin from accessing metered data.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+ * @return the list of restricted package names.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public @NonNull List<String> getMeteredDataDisabled(@NonNull ComponentName admin) {
+ throwIfParentInstance("getMeteredDataDisabled");
+ if (mService != null) {
+ try {
+ return mService.getMeteredDataDisabled(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return new ArrayList<>();
+ }
+
+ /**
* Called by device owners to retrieve device logs from before the device's last reboot.
* <p>
* <strong> This API is not supported on all devices. Calling this API on unsupported devices
@@ -8511,6 +8682,13 @@
*
* <p> Backup service is off by default when device owner is present.
*
+ * <p> If backups are made mandatory by specifying a non-null mandatory backup transport using
+ * the {@link DevicePolicyManager#setMandatoryBackupTransport} method, the backup service is
+ * automatically enabled.
+ *
+ * <p> If the backup service is disabled using this method after the mandatory backup transport
+ * has been set, the mandatory backup transport is cleared.
+ *
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param enabled {@code true} to enable the backup service, {@code false} to disable it.
* @throws SecurityException if {@code admin} is not a device owner.
@@ -8542,6 +8720,43 @@
}
/**
+ * Makes backups mandatory and enforces the usage of the specified backup transport.
+ *
+ * <p>When a {@code null} backup transport is specified, backups are made optional again.
+ * <p>Only device owner can call this method.
+ * <p>If backups were disabled and a non-null backup transport {@link ComponentName} is
+ * specified, backups will be enabled.
+ *
+ * @param admin admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param backupTransportComponent The backup transport layer to be used for mandatory backups.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public void setMandatoryBackupTransport(
+ @NonNull ComponentName admin, @Nullable ComponentName backupTransportComponent) {
+ try {
+ mService.setMandatoryBackupTransport(admin, backupTransportComponent);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the backup transport which has to be used for backups if backups are mandatory or
+ * {@code null} if backups are not mandatory.
+ *
+ * @return a {@link ComponentName} of the backup transport layer to be used if backups are
+ * mandatory or {@code null} if backups are not mandatory.
+ */
+ public ComponentName getMandatoryBackupTransport() {
+ try {
+ return mService.getMandatoryBackupTransport();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+
+ /**
* Called by a device owner to control the network logging feature.
*
* <p> Network logs contain DNS lookup and connect() library call events. The following library
@@ -8817,15 +9032,6 @@
}
}
- /** {@hide} */
- @Condemned
- @Deprecated
- public boolean clearApplicationUserData(@NonNull ComponentName admin,
- @NonNull String packageName, @NonNull OnClearApplicationUserDataListener listener,
- @NonNull Handler handler) {
- return clearApplicationUserData(admin, packageName, listener, new HandlerExecutor(handler));
- }
-
/**
* Called by the device owner or profile owner to clear application user data of a given
* package. The behaviour of this is equivalent to the target application calling
@@ -8836,14 +9042,14 @@
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param packageName The name of the package which will have its user data wiped.
- * @param listener A callback object that will inform the caller when the clearing is done.
* @param executor The executor through which the listener should be invoked.
+ * @param listener A callback object that will inform the caller when the clearing is done.
* @throws SecurityException if the caller is not the device owner/profile owner.
* @return whether the clearing succeeded.
*/
public boolean clearApplicationUserData(@NonNull ComponentName admin,
- @NonNull String packageName, @NonNull OnClearApplicationUserDataListener listener,
- @NonNull @CallbackExecutor Executor executor) {
+ @NonNull String packageName, @NonNull @CallbackExecutor Executor executor,
+ @NonNull OnClearApplicationUserDataListener listener) {
throwIfParentInstance("clearAppData");
Preconditions.checkNotNull(executor);
try {
@@ -8926,41 +9132,312 @@
}
}
- //TODO STOPSHIP Add link to onTransferComplete callback when implemented.
/**
- * Transfers the current administrator. All policies from the current administrator are
- * migrated to the new administrator. The whole operation is atomic - the transfer is either
- * complete or not done at all.
+ * Changes the current administrator to another one. All policies from the current
+ * administrator are migrated to the new administrator. The whole operation is atomic -
+ * the transfer is either complete or not done at all.
*
- * Depending on the current administrator (device owner, profile owner, corporate owned
- * profile owner), you have the following expected behaviour:
+ * <p>Depending on the current administrator (device owner, profile owner), you have the
+ * following expected behaviour:
* <ul>
* <li>A device owner can only be transferred to a new device owner</li>
* <li>A profile owner can only be transferred to a new profile owner</li>
- * <li>A corporate owned managed profile can have two cases:
- * <ul>
- * <li>If the device owner and profile owner are the same package,
- * both will be transferred.</li>
- * <li>If the device owner and profile owner are different packages,
- * and if this method is called from the profile owner, only the profile owner
- * is transferred. Similarly, if it is called from the device owner, only
- * the device owner is transferred.</li>
- * </ul>
- * </li>
* </ul>
*
- * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
- * @param target Which {@link DeviceAdminReceiver} we want the new administrator to be.
- * @param bundle Parameters - This bundle allows the current administrator to pass data to the
- * new administrator. The parameters will be received in the
- * onTransferComplete callback.
- * @hide
+ * <p>Use the {@code bundle} parameter to pass data to the new administrator. The data
+ * will be received in the
+ * {@link DeviceAdminReceiver#onTransferOwnershipComplete(Context, PersistableBundle)}
+ * callback of the new administrator.
+ *
+ * <p>The transfer has failed if the original administrator is still the corresponding owner
+ * after calling this method.
+ *
+ * <p>The incoming target administrator must have the
+ * {@link DeviceAdminReceiver#SUPPORT_TRANSFER_OWNERSHIP_META_DATA} <code>meta-data</code> tag
+ * included in its corresponding <code>receiver</code> component with a value of {@code true}.
+ * Otherwise an {@link IllegalArgumentException} will be thrown.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with
+ * @param target which {@link DeviceAdminReceiver} we want the new administrator to be
+ * @param bundle data to be sent to the new administrator
+ * @throws SecurityException if {@code admin} is not a device owner nor a profile owner
+ * @throws IllegalArgumentException if {@code admin} or {@code target} is {@code null}, they
+ * are components in the same package or {@code target} is not an active admin
*/
- public void transferOwner(@NonNull ComponentName admin, @NonNull ComponentName target,
- PersistableBundle bundle) {
- throwIfParentInstance("transferOwner");
+ public void transferOwnership(@NonNull ComponentName admin, @NonNull ComponentName target,
+ @Nullable PersistableBundle bundle) {
+ throwIfParentInstance("transferOwnership");
try {
- mService.transferOwner(admin, target, bundle);
+ mService.transferOwnership(admin, target, bundle);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by a device owner to specify the user session start message. This may be displayed
+ * during a user switch.
+ * <p>
+ * The message should be limited to a short statement or it may be truncated.
+ * <p>
+ * If the message needs to be localized, it is the responsibility of the
+ * {@link DeviceAdminReceiver} to listen to the {@link Intent#ACTION_LOCALE_CHANGED} broadcast
+ * and set a new version of this message accordingly.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+ * @param startUserSessionMessage message for starting user session, or {@code null} to use
+ * system default message.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public void setStartUserSessionMessage(
+ @NonNull ComponentName admin, @Nullable CharSequence startUserSessionMessage) {
+ throwIfParentInstance("setStartUserSessionMessage");
+ try {
+ mService.setStartUserSessionMessage(admin, startUserSessionMessage);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by a device owner to specify the user session end message. This may be displayed
+ * during a user switch.
+ * <p>
+ * The message should be limited to a short statement or it may be truncated.
+ * <p>
+ * If the message needs to be localized, it is the responsibility of the
+ * {@link DeviceAdminReceiver} to listen to the {@link Intent#ACTION_LOCALE_CHANGED} broadcast
+ * and set a new version of this message accordingly.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+ * @param endUserSessionMessage message for ending user session, or {@code null} to use system
+ * default message.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public void setEndUserSessionMessage(
+ @NonNull ComponentName admin, @Nullable CharSequence endUserSessionMessage) {
+ throwIfParentInstance("setEndUserSessionMessage");
+ try {
+ mService.setEndUserSessionMessage(admin, endUserSessionMessage);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the user session start message.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public CharSequence getStartUserSessionMessage(@NonNull ComponentName admin) {
+ throwIfParentInstance("getStartUserSessionMessage");
+ try {
+ return mService.getStartUserSessionMessage(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the user session end message.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public CharSequence getEndUserSessionMessage(@NonNull ComponentName admin) {
+ throwIfParentInstance("getEndUserSessionMessage");
+ try {
+ return mService.getEndUserSessionMessage(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Allows/disallows printing.
+ *
+ * Called by a device owner or a profile owner.
+ * Device owner changes policy for all users. Profile owner can override it if present.
+ * Printing is enabled by default. If {@code FEATURE_PRINTING} is absent, the call is ignored.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+ * @param enabled whether printing should be allowed or not.
+ * @throws SecurityException if {@code admin} is neither device, nor profile owner.
+ */
+ public void setPrintingEnabled(@NonNull ComponentName admin, boolean enabled) {
+ try {
+ mService.setPrintingEnabled(admin, enabled);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns whether printing is enabled for this user.
+ *
+ * Always {@code false} if {@code FEATURE_PRINTING} is absent.
+ * Otherwise, {@code true} by default.
+ *
+ * @return {@code true} iff printing is enabled.
+ */
+ public boolean isPrintingEnabled() {
+ try {
+ return mService.isPrintingEnabled();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by device owner to add an override APN.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with
+ * @param apnSetting the override APN to insert
+ * @return The {@code id} of inserted override APN. Or {@code -1} when failed to insert into
+ * the database.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ *
+ * @see #setOverrideApnsEnabled(ComponentName, boolean)
+ */
+ public int addOverrideApn(@NonNull ComponentName admin, @NonNull ApnSetting apnSetting) {
+ throwIfParentInstance("addOverrideApn");
+ if (mService != null) {
+ try {
+ return mService.addOverrideApn(admin, apnSetting);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Called by device owner to update an override APN.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with
+ * @param apnId the {@code id} of the override APN to update
+ * @param apnSetting the override APN to update
+ * @return {@code true} if the required override APN is successfully updated,
+ * {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ *
+ * @see #setOverrideApnsEnabled(ComponentName, boolean)
+ */
+ public boolean updateOverrideApn(@NonNull ComponentName admin, int apnId,
+ @NonNull ApnSetting apnSetting) {
+ throwIfParentInstance("updateOverrideApn");
+ if (mService != null) {
+ try {
+ return mService.updateOverrideApn(admin, apnId, apnSetting);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by device owner to remove an override APN.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with
+ * @param apnId the {@code id} of the override APN to remove
+ * @return {@code true} if the required override APN is successfully removed, {@code false}
+ * otherwise.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ *
+ * @see #setOverrideApnsEnabled(ComponentName, boolean)
+ */
+ public boolean removeOverrideApn(@NonNull ComponentName admin, int apnId) {
+ throwIfParentInstance("removeOverrideApn");
+ if (mService != null) {
+ try {
+ return mService.removeOverrideApn(admin, apnId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by device owner to get all override APNs inserted by device owner.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with
+ * @return A list of override APNs inserted by device owner.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ *
+ * @see #setOverrideApnsEnabled(ComponentName, boolean)
+ */
+ public List<ApnSetting> getOverrideApns(@NonNull ComponentName admin) {
+ throwIfParentInstance("getOverrideApns");
+ if (mService != null) {
+ try {
+ return mService.getOverrideApns(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Called by device owner to set if override APNs should be enabled.
+ * <p> Override APNs are separated from other APNs on the device, and can only be inserted or
+ * modified by the device owner. When enabled, only override APNs are in use, any other APNs
+ * are ignored.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with
+ * @param enabled {@code true} if override APNs should be enabled, {@code false} otherwise
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public void setOverrideApnsEnabled(@NonNull ComponentName admin, boolean enabled) {
+ throwIfParentInstance("setOverrideApnEnabled");
+ if (mService != null) {
+ try {
+ mService.setOverrideApnsEnabled(admin, enabled);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by device owner to check if override APNs are currently enabled.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with
+ * @return {@code true} if override APNs are currently enabled, {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ *
+ * @see #setOverrideApnsEnabled(ComponentName, boolean)
+ */
+ public boolean isOverrideApnEnabled(@NonNull ComponentName admin) {
+ throwIfParentInstance("isOverrideApnEnabled");
+ if (mService != null) {
+ try {
+ return mService.isOverrideApnEnabled(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the data passed from the current administrator to the new administrator during an
+ * ownership transfer. This is the same {@code bundle} passed in
+ * {@link #transferOwnership(ComponentName, ComponentName, PersistableBundle)}.
+ *
+ * <p>Returns <code>null</code> if no ownership transfer was started for the calling user.
+ *
+ * @see #transferOwnership
+ * @see DeviceAdminReceiver#onTransferOwnershipComplete(Context, PersistableBundle)
+ */
+ @Nullable
+ public PersistableBundle getTransferOwnershipBundle() {
+ throwIfParentInstance("getTransferOwnershipBundle");
+ try {
+ return mService.getTransferOwnershipBundle();
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
diff --git a/android/app/admin/DevicePolicyManagerInternal.java b/android/app/admin/DevicePolicyManagerInternal.java
index b692ffd..ebaf464 100644
--- a/android/app/admin/DevicePolicyManagerInternal.java
+++ b/android/app/admin/DevicePolicyManagerInternal.java
@@ -123,4 +123,22 @@
* @param userId User ID of the profile.
*/
public abstract void reportSeparateProfileChallengeChanged(@UserIdInt int userId);
+
+ /**
+ * Check whether the user could have their password reset in an untrusted manor due to there
+ * being an admin which can call {@link #resetPassword} to reset the password without knowledge
+ * of the previous password.
+ *
+ * @param userId The user in question
+ */
+ public abstract boolean canUserHaveUntrustedCredentialReset(@UserIdInt int userId);
+
+ /**
+ * Return text of error message if printing is disabled.
+ * Called by Print Service when printing is disabled by PO or DO when printing is attempted.
+ *
+ * @param userId The user in question
+ * @return localized error message
+ */
+ public abstract CharSequence getPrintingDisabledReasonForUser(@UserIdInt int userId);
}
diff --git a/android/app/admin/DnsEvent.java b/android/app/admin/DnsEvent.java
index 4ddf13e..a2d704b 100644
--- a/android/app/admin/DnsEvent.java
+++ b/android/app/admin/DnsEvent.java
@@ -96,7 +96,7 @@
@Override
public String toString() {
- return String.format("DnsEvent(%s, %s, %d, %d, %s)", mHostname,
+ return String.format("DnsEvent(%d, %s, %s, %d, %d, %s)", mId, mHostname,
(mIpAddresses == null) ? "NONE" : String.join(" ", mIpAddresses),
mIpAddressesCount, mTimestamp, mPackageName);
}
diff --git a/android/app/assist/AssistStructure.java b/android/app/assist/AssistStructure.java
index 7b549cd..87f2271 100644
--- a/android/app/assist/AssistStructure.java
+++ b/android/app/assist/AssistStructure.java
@@ -32,6 +32,8 @@
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
+import com.android.internal.util.Preconditions;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -624,6 +626,7 @@
int mMinEms = -1;
int mMaxEms = -1;
int mMaxLength = -1;
+ @Nullable String mTextIdEntry;
// POJO used to override some autofill-related values when the node is parcelized.
// Not written to parcel.
@@ -701,7 +704,7 @@
final int flags = mFlags;
if ((flags&FLAGS_HAS_ID) != 0) {
mId = in.readInt();
- if (mId != 0) {
+ if (mId != View.NO_ID) {
mIdEntry = preader.readString();
if (mIdEntry != null) {
mIdType = preader.readString();
@@ -724,6 +727,7 @@
mMinEms = in.readInt();
mMaxEms = in.readInt();
mMaxLength = in.readInt();
+ mTextIdEntry = preader.readString();
}
if ((flags&FLAGS_HAS_LARGE_COORDS) != 0) {
mX = in.readInt();
@@ -857,7 +861,7 @@
out.writeInt(writtenFlags);
if ((flags&FLAGS_HAS_ID) != 0) {
out.writeInt(mId);
- if (mId != 0) {
+ if (mId != View.NO_ID) {
pwriter.writeString(mIdEntry);
if (mIdEntry != null) {
pwriter.writeString(mIdType);
@@ -890,6 +894,7 @@
out.writeInt(mMinEms);
out.writeInt(mMaxEms);
out.writeInt(mMaxLength);
+ pwriter.writeString(mTextIdEntry);
}
if ((flags&FLAGS_HAS_LARGE_COORDS) != 0) {
out.writeInt(mX);
@@ -1430,6 +1435,17 @@
}
/**
+ * Gets the identifier used to set the text associated with this view.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for autofill purposes,
+ * not for assist purposes.
+ */
+ @Nullable
+ public String getTextIdEntry() {
+ return mTextIdEntry;
+ }
+
+ /**
* Return additional hint text associated with the node; this is typically used with
* a node that takes user input, describing to the user what the input means.
*/
@@ -1684,6 +1700,11 @@
}
@Override
+ public void setTextIdEntry(@NonNull String entryName) {
+ mNode.mTextIdEntry = Preconditions.checkNotNull(entryName);
+ }
+
+ @Override
public void setHint(CharSequence hint) {
getNodeText().mHint = hint != null ? hint.toString() : null;
}
@@ -2082,6 +2103,7 @@
Log.i(TAG, prefix + " Text color fg: #" + Integer.toHexString(node.getTextColor())
+ ", bg: #" + Integer.toHexString(node.getTextBackgroundColor()));
Log.i(TAG, prefix + " Input type: " + node.getInputType());
+ Log.i(TAG, prefix + " Resource id: " + node.getTextIdEntry());
}
String webDomain = node.getWebDomain();
if (webDomain != null) {
diff --git a/android/app/backup/BackupManager.java b/android/app/backup/BackupManager.java
index 6512b98..12f4483 100644
--- a/android/app/backup/BackupManager.java
+++ b/android/app/backup/BackupManager.java
@@ -27,6 +27,7 @@
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.UserHandle;
import android.util.Log;
import android.util.Pair;
@@ -387,6 +388,29 @@
}
/**
+ * Report whether the backup mechanism is currently active.
+ * When it is inactive, the device will not perform any backup operations, nor will it
+ * deliver data for restore, although clients can still safely call BackupManager methods.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public boolean isBackupServiceActive(UserHandle user) {
+ mContext.enforceCallingPermission(android.Manifest.permission.BACKUP,
+ "isBackupServiceActive");
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ return sService.isBackupServiceActive(user.getIdentifier());
+ } catch (RemoteException e) {
+ Log.e(TAG, "isBackupEnabled() couldn't connect");
+ }
+ }
+ return false;
+ }
+
+ /**
* Enable/disable data restore at application install time. When enabled, app
* installation will include an attempt to fetch the app's historical data from
* the archival restore dataset (if any). When disabled, no such attempt will
@@ -707,7 +731,6 @@
* redirects them into main-thread actions. This serializes the backup
* progress callbacks nicely within the usual main-thread lifecycle pattern.
*/
- @SystemApi
private class BackupObserverWrapper extends IBackupObserver.Stub {
final Handler mHandler;
final BackupObserver mObserver;
diff --git a/android/app/backup/BackupManagerMonitor.java b/android/app/backup/BackupManagerMonitor.java
index ae4a98a..a91aded 100644
--- a/android/app/backup/BackupManagerMonitor.java
+++ b/android/app/backup/BackupManagerMonitor.java
@@ -172,6 +172,12 @@
public static final int LOG_EVENT_ID_NO_PACKAGES = 49;
public static final int LOG_EVENT_ID_TRANSPORT_IS_NULL = 50;
+ /**
+ * The transport returned {@link BackupTransport#TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED}.
+ * @hide
+ */
+ public static final int LOG_EVENT_ID_TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED = 51;
+
diff --git a/android/app/backup/BackupTransport.java b/android/app/backup/BackupTransport.java
index da81d19..266f58d 100644
--- a/android/app/backup/BackupTransport.java
+++ b/android/app/backup/BackupTransport.java
@@ -51,10 +51,40 @@
public static final int AGENT_UNKNOWN = -1004;
public static final int TRANSPORT_QUOTA_EXCEEDED = -1005;
+ /**
+ * Indicates that the transport cannot accept a diff backup for this package.
+ *
+ * <p>Backup manager should clear its state for this package and immediately retry a
+ * non-incremental backup. This might be used if the transport no longer has data for this
+ * package in its backing store.
+ *
+ * <p>This is only valid when backup manager called {@link
+ * #performBackup(PackageInfo, ParcelFileDescriptor, int)} with {@link #FLAG_INCREMENTAL}.
+ *
+ * @hide
+ */
+ public static final int TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED = -1006;
+
// Indicates that operation was initiated by user, not a scheduled one.
// Transport should ignore its own moratoriums for call with this flag set.
public static final int FLAG_USER_INITIATED = 1;
+ /**
+ * For key value backup, indicates that the backup data is a diff from a previous backup. The
+ * transport must apply this diff to an existing backup to build the new backup set.
+ *
+ * @hide
+ */
+ public static final int FLAG_INCREMENTAL = 1 << 1;
+
+ /**
+ * For key value backup, indicates that the backup data is a complete set, not a diff from a
+ * previous backup. The transport should clear any previous backup when storing this backup.
+ *
+ * @hide
+ */
+ public static final int FLAG_NON_INCREMENTAL = 1 << 2;
+
IBackupTransport mBinderImpl = new TransportImpl();
public IBinder getBinder() {
@@ -231,18 +261,33 @@
* {@link #TRANSPORT_OK}, {@link #finishBackup} will then be called to ensure the data
* is sent and recorded successfully.
*
+ * If the backup data is a diff against the previous backup then the flag {@link
+ * BackupTransport#FLAG_INCREMENTAL} will be set. Otherwise, if the data is a complete backup
+ * set then {@link BackupTransport#FLAG_NON_INCREMENTAL} will be set. Before P neither flag will
+ * be set regardless of whether the backup is incremental or not.
+ *
+ * <p>If {@link BackupTransport#FLAG_INCREMENTAL} is set and the transport does not have data
+ * for this package in its storage backend then it cannot apply the incremental diff. Thus it
+ * should return {@link BackupTransport#TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED} to indicate
+ * that backup manager should delete its state and retry the package as a non-incremental
+ * backup. Before P, or if this is a non-incremental backup, then this return code is equivalent
+ * to {@link BackupTransport#TRANSPORT_ERROR}.
+ *
* @param packageInfo The identity of the application whose data is being backed up.
* This specifically includes the signature list for the package.
* @param inFd Descriptor of file with data that resulted from invoking the application's
* BackupService.doBackup() method. This may be a pipe rather than a file on
* persistent media, so it may not be seekable.
- * @param flags {@link BackupTransport#FLAG_USER_INITIATED} or 0.
+ * @param flags a combination of {@link BackupTransport#FLAG_USER_INITIATED}, {@link
+ * BackupTransport#FLAG_NON_INCREMENTAL}, {@link BackupTransport#FLAG_INCREMENTAL}, or 0.
* @return one of {@link BackupTransport#TRANSPORT_OK} (OK so far),
* {@link BackupTransport#TRANSPORT_PACKAGE_REJECTED} (to suppress backup of this
* specific package, but allow others to proceed),
- * {@link BackupTransport#TRANSPORT_ERROR} (on network error or other failure), or
- * {@link BackupTransport#TRANSPORT_NOT_INITIALIZED} (if the backend dataset has
- * become lost due to inactivity purge or some other reason and needs re-initializing)
+ * {@link BackupTransport#TRANSPORT_ERROR} (on network error or other failure), {@link
+ * BackupTransport#TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED} (if the transport cannot accept
+ * an incremental backup for this package), or {@link
+ * BackupTransport#TRANSPORT_NOT_INITIALIZED} (if the backend dataset has become lost due to
+ * inactivity purge or some other reason and needs re-initializing)
*/
public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor inFd, int flags) {
return performBackup(packageInfo, inFd);
diff --git a/android/app/job/JobInfo.java b/android/app/job/JobInfo.java
index 7c40b4e..cba9dcc 100644
--- a/android/app/job/JobInfo.java
+++ b/android/app/job/JobInfo.java
@@ -253,6 +253,11 @@
/**
* @hide
*/
+ public static final int FLAG_IS_PREFETCH = 1 << 2;
+
+ /**
+ * @hide
+ */
public static final int CONSTRAINT_FLAG_CHARGING = 1 << 0;
/**
@@ -1364,6 +1369,28 @@
}
/**
+ * Setting this to true indicates that this job is designed to prefetch
+ * content that will make a material improvement to the experience of
+ * the specific user of this device. For example, fetching top headlines
+ * of interest to the current user.
+ * <p>
+ * The system may use this signal to relax the network constraints you
+ * originally requested, such as allowing a
+ * {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over a metered
+ * network when there is a surplus of metered data available. The system
+ * may also use this signal in combination with end user usage patterns
+ * to ensure data is prefetched before the user launches your app.
+ */
+ public Builder setIsPrefetch(boolean isPrefetch) {
+ if (isPrefetch) {
+ mFlags |= FLAG_IS_PREFETCH;
+ } else {
+ mFlags &= (~FLAG_IS_PREFETCH);
+ }
+ return this;
+ }
+
+ /**
* Set whether or not to persist this job across device reboots.
*
* @param isPersisted True to indicate that the job will be written to
diff --git a/android/app/job/JobParameters.java b/android/app/job/JobParameters.java
index 5053dc6..c71bf2e 100644
--- a/android/app/job/JobParameters.java
+++ b/android/app/job/JobParameters.java
@@ -70,6 +70,7 @@
private final Network network;
private int stopReason; // Default value of stopReason is REASON_CANCELED
+ private String debugStopReason; // Human readable stop reason for debugging.
/** @hide */
public JobParameters(IBinder callback, int jobId, PersistableBundle extras,
@@ -104,6 +105,14 @@
}
/**
+ * Reason onStopJob() was called on this job.
+ * @hide
+ */
+ public String getDebugStopReason() {
+ return debugStopReason;
+ }
+
+ /**
* @return The extras you passed in when constructing this job with
* {@link android.app.job.JobInfo.Builder#setExtras(android.os.PersistableBundle)}. This will
* never be null. If you did not set any extras this will be an empty bundle.
@@ -288,11 +297,13 @@
network = null;
}
stopReason = in.readInt();
+ debugStopReason = in.readString();
}
/** @hide */
- public void setStopReason(int reason) {
+ public void setStopReason(int reason, String debugStopReason) {
stopReason = reason;
+ this.debugStopReason = debugStopReason;
}
@Override
@@ -323,6 +334,7 @@
dest.writeInt(0);
}
dest.writeInt(stopReason);
+ dest.writeString(debugStopReason);
}
public static final Creator<JobParameters> CREATOR = new Creator<JobParameters>() {
diff --git a/android/app/servertransaction/ActivityLifecycleItem.java b/android/app/servertransaction/ActivityLifecycleItem.java
index 0fdc7c5..9a50a00 100644
--- a/android/app/servertransaction/ActivityLifecycleItem.java
+++ b/android/app/servertransaction/ActivityLifecycleItem.java
@@ -17,7 +17,9 @@
package android.app.servertransaction;
import android.annotation.IntDef;
+import android.os.Parcel;
+import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -26,6 +28,7 @@
* @hide
*/
public abstract class ActivityLifecycleItem extends ClientTransactionItem {
+ private String mDescription;
@IntDef(prefix = { "UNDEFINED", "PRE_", "ON_" }, value = {
UNDEFINED,
@@ -53,4 +56,39 @@
/** A final lifecycle state that an activity should reach. */
@LifecycleState
public abstract int getTargetState();
+
+
+ protected ActivityLifecycleItem() {
+ }
+
+ protected ActivityLifecycleItem(Parcel in) {
+ mDescription = in.readString();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mDescription);
+ }
+
+ /**
+ * Sets a description that can be retrieved later for debugging purposes.
+ * @param description Description to set.
+ * @return The {@link ActivityLifecycleItem}.
+ */
+ public ActivityLifecycleItem setDescription(String description) {
+ mDescription = description;
+ return this;
+ }
+
+ /**
+ * Retrieves description if set through {@link #setDescription(String)}.
+ */
+ public String getDescription() {
+ return mDescription;
+ }
+
+ void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + "target state:" + getTargetState());
+ pw.println(prefix + "description: " + mDescription);
+ }
}
diff --git a/android/app/servertransaction/ClientTransaction.java b/android/app/servertransaction/ClientTransaction.java
index 3c96f06..fc07879 100644
--- a/android/app/servertransaction/ClientTransaction.java
+++ b/android/app/servertransaction/ClientTransaction.java
@@ -24,6 +24,9 @@
import android.os.Parcelable;
import android.os.RemoteException;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -83,7 +86,8 @@
}
/** Get the target state lifecycle request. */
- ActivityLifecycleItem getLifecycleStateRequest() {
+ @VisibleForTesting
+ public ActivityLifecycleItem getLifecycleStateRequest() {
return mLifecycleStateRequest;
}
@@ -234,4 +238,12 @@
result = 31 * result + Objects.hashCode(mLifecycleStateRequest);
return result;
}
+
+ void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + "mActivityToken:" + mActivityToken.hashCode());
+ pw.println(prefix + "mLifecycleStateRequest:");
+ if (mLifecycleStateRequest != null) {
+ mLifecycleStateRequest.dump(pw, prefix + " ");
+ }
+ }
}
diff --git a/android/app/servertransaction/DestroyActivityItem.java b/android/app/servertransaction/DestroyActivityItem.java
index 83da5f3..cbcf6c7 100644
--- a/android/app/servertransaction/DestroyActivityItem.java
+++ b/android/app/servertransaction/DestroyActivityItem.java
@@ -76,12 +76,14 @@
/** Write to Parcel. */
@Override
public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
dest.writeBoolean(mFinished);
dest.writeInt(mConfigChanges);
}
/** Read from Parcel. */
private DestroyActivityItem(Parcel in) {
+ super(in);
mFinished = in.readBoolean();
mConfigChanges = in.readInt();
}
diff --git a/android/app/servertransaction/PauseActivityItem.java b/android/app/servertransaction/PauseActivityItem.java
index 880fef7..70a4755 100644
--- a/android/app/servertransaction/PauseActivityItem.java
+++ b/android/app/servertransaction/PauseActivityItem.java
@@ -114,6 +114,7 @@
/** Write to Parcel. */
@Override
public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
dest.writeBoolean(mFinished);
dest.writeBoolean(mUserLeaving);
dest.writeInt(mConfigChanges);
@@ -122,6 +123,7 @@
/** Read from Parcel. */
private PauseActivityItem(Parcel in) {
+ super(in);
mFinished = in.readBoolean();
mUserLeaving = in.readBoolean();
mConfigChanges = in.readInt();
diff --git a/android/app/servertransaction/ResumeActivityItem.java b/android/app/servertransaction/ResumeActivityItem.java
index 9249c6e..ed90f2c 100644
--- a/android/app/servertransaction/ResumeActivityItem.java
+++ b/android/app/servertransaction/ResumeActivityItem.java
@@ -113,6 +113,7 @@
/** Write to Parcel. */
@Override
public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
dest.writeInt(mProcState);
dest.writeBoolean(mUpdateProcState);
dest.writeBoolean(mIsForward);
@@ -120,6 +121,7 @@
/** Read from Parcel. */
private ResumeActivityItem(Parcel in) {
+ super(in);
mProcState = in.readInt();
mUpdateProcState = in.readBoolean();
mIsForward = in.readBoolean();
diff --git a/android/app/servertransaction/StopActivityItem.java b/android/app/servertransaction/StopActivityItem.java
index 5c5c304..b814d1a 100644
--- a/android/app/servertransaction/StopActivityItem.java
+++ b/android/app/servertransaction/StopActivityItem.java
@@ -83,12 +83,14 @@
/** Write to Parcel. */
@Override
public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
dest.writeBoolean(mShowWindow);
dest.writeInt(mConfigChanges);
}
/** Read from Parcel. */
private StopActivityItem(Parcel in) {
+ super(in);
mShowWindow = in.readBoolean();
mConfigChanges = in.readInt();
}
diff --git a/android/app/servertransaction/TransactionExecutor.java b/android/app/servertransaction/TransactionExecutor.java
index 5b0ea6b..78b393a 100644
--- a/android/app/servertransaction/TransactionExecutor.java
+++ b/android/app/servertransaction/TransactionExecutor.java
@@ -33,6 +33,8 @@
import com.android.internal.annotations.VisibleForTesting;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.util.List;
/**
@@ -122,6 +124,21 @@
final IBinder token = transaction.getActivityToken();
final ActivityClientRecord r = mTransactionHandler.getActivityClient(token);
+ // TODO(b/71506345): Remove once root cause is found.
+ if (r == null) {
+ final StringWriter stringWriter = new StringWriter();
+ final PrintWriter pw = new PrintWriter(stringWriter);
+ final String prefix = " ";
+
+ pw.println("Lifecycle transaction does not have valid ActivityClientRecord.");
+ pw.println("Transaction:");
+ transaction.dump(pw, prefix);
+ pw.println("Executor:");
+ dump(pw, prefix);
+
+ Slog.wtf(TAG, stringWriter.toString());
+ }
+
// Cycle to the state right before the final requested state.
cycleToPath(r, lifecycleItem.getTargetState(), true /* excludeLastState */);
@@ -245,4 +262,9 @@
private static void log(String message) {
if (DEBUG_RESOLVER) Slog.d(TAG, message);
}
+
+ private void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + "mTransactionHandler:");
+ mTransactionHandler.dump(pw, prefix + " ");
+ }
}
diff --git a/android/app/slice/Slice.java b/android/app/slice/Slice.java
index 5c7f674..5808f8b 100644
--- a/android/app/slice/Slice.java
+++ b/android/app/slice/Slice.java
@@ -21,12 +21,10 @@
import android.annotation.StringDef;
import android.app.PendingIntent;
import android.app.RemoteInput;
-import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
import android.content.IContentProvider;
import android.content.Intent;
-import android.content.pm.ResolveInfo;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
@@ -67,6 +65,7 @@
HINT_TOGGLE,
HINT_HORIZONTAL,
HINT_PARTIAL,
+ HINT_SEE_MORE
})
@Retention(RetentionPolicy.SOURCE)
public @interface SliceHint {}
@@ -151,7 +150,19 @@
* Used to indicate the maximum integer value for a {@link #SUBTYPE_SLIDER}.
*/
public static final String HINT_MAX = "max";
-
+ /**
+ * A hint representing that this item should be used to indicate that there's more
+ * content associated with this slice.
+ */
+ public static final String HINT_SEE_MORE = "see_more";
+ /**
+ * A hint used when implementing app-specific slice permissions.
+ * Tells the system that for this slice the return value of
+ * {@link SliceProvider#onBindSlice(Uri, List)} may be different depending on
+ * {@link SliceProvider#getBindingPackage} and should not be cached for multiple
+ * apps.
+ */
+ public static final String HINT_CALLER_NEEDED = "caller_needed";
/**
* Key to retrieve an extra added to an intent when a control is changed.
*/
@@ -184,6 +195,10 @@
* Subtype to tag an item representing priority.
*/
public static final String SUBTYPE_PRIORITY = "priority";
+ /**
+ * Subtype to tag an item to use as a content description.
+ */
+ public static final String SUBTYPE_CONTENT_DESCRIPTION = "content_description";
private final SliceItem[] mItems;
private final @SliceHint String[] mHints;
@@ -415,28 +430,6 @@
* Add a color to the slice being constructed
* @param subType Optional template-specific type information
* @see {@link SliceItem#getSubType()}
- * @deprecated will be removed once supportlib updates
- */
- public Builder addColor(int color, @Nullable String subType, @SliceHint String... hints) {
- mItems.add(new SliceItem(color, SliceItem.FORMAT_INT, subType, hints));
- return this;
- }
-
- /**
- * Add a color to the slice being constructed
- * @param subType Optional template-specific type information
- * @see {@link SliceItem#getSubType()}
- * @deprecated will be removed once supportlib updates
- */
- public Builder addColor(int color, @Nullable String subType,
- @SliceHint List<String> hints) {
- return addColor(color, subType, hints.toArray(new String[hints.size()]));
- }
-
- /**
- * Add a color to the slice being constructed
- * @param subType Optional template-specific type information
- * @see {@link SliceItem#getSubType()}
*/
public Builder addInt(int value, @Nullable String subType, @SliceHint String... hints) {
mItems.add(new SliceItem(value, SliceItem.FORMAT_INT, subType, hints));
@@ -549,16 +542,11 @@
}
/**
- * Turns a slice Uri into slice content.
- *
- * @param resolver ContentResolver to be used.
- * @param uri The URI to a slice provider
- * @param supportedSpecs List of supported specs.
- * @return The Slice provided by the app or null if none is given.
- * @see Slice
+ * @deprecated TO BE REMOVED.
*/
- public static @Nullable Slice bindSlice(ContentResolver resolver, @NonNull Uri uri,
- List<SliceSpec> supportedSpecs) {
+ @Deprecated
+ public static @Nullable Slice bindSlice(ContentResolver resolver,
+ @NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
Preconditions.checkNotNull(uri, "uri");
IContentProvider provider = resolver.acquireProvider(uri);
if (provider == null) {
@@ -586,60 +574,11 @@
}
/**
- * Turns a slice intent into slice content. Expects an explicit intent. If there is no
- * {@link ContentProvider} associated with the given intent this will throw
- * {@link IllegalArgumentException}.
- *
- * @param context The context to use.
- * @param intent The intent associated with a slice.
- * @param supportedSpecs List of supported specs.
- * @return The Slice provided by the app or null if none is given.
- * @see Slice
- * @see SliceProvider#onMapIntentToUri(Intent)
- * @see Intent
+ * @deprecated TO BE REMOVED.
*/
+ @Deprecated
public static @Nullable Slice bindSlice(Context context, @NonNull Intent intent,
- List<SliceSpec> supportedSpecs) {
- Preconditions.checkNotNull(intent, "intent");
- Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null,
- "Slice intent must be explicit " + intent);
- ContentResolver resolver = context.getContentResolver();
-
- // Check if the intent has data for the slice uri on it and use that
- final Uri intentData = intent.getData();
- if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
- return bindSlice(resolver, intentData, supportedSpecs);
- }
- // Otherwise ask the app
- List<ResolveInfo> providers =
- context.getPackageManager().queryIntentContentProviders(intent, 0);
- if (providers == null) {
- throw new IllegalArgumentException("Unable to resolve intent " + intent);
- }
- String authority = providers.get(0).providerInfo.authority;
- Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
- .authority(authority).build();
- IContentProvider provider = resolver.acquireProvider(uri);
- if (provider == null) {
- throw new IllegalArgumentException("Unknown URI " + uri);
- }
- try {
- Bundle extras = new Bundle();
- extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
- extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
- new ArrayList<>(supportedSpecs));
- final Bundle res = provider.call(resolver.getPackageName(),
- SliceProvider.METHOD_MAP_INTENT, null, extras);
- if (res == null) {
- return null;
- }
- return res.getParcelable(SliceProvider.EXTRA_SLICE);
- } catch (RemoteException e) {
- // Arbitrary and not worth documenting, as Activity
- // Manager will kill this process shortly anyway.
- return null;
- } finally {
- resolver.releaseProvider(provider);
- }
+ @NonNull List<SliceSpec> supportedSpecs) {
+ return context.getSystemService(SliceManager.class).bindSlice(intent, supportedSpecs);
}
}
diff --git a/android/app/slice/SliceItem.java b/android/app/slice/SliceItem.java
index bcfd413..9eb2bb8 100644
--- a/android/app/slice/SliceItem.java
+++ b/android/app/slice/SliceItem.java
@@ -98,11 +98,6 @@
*/
public static final String FORMAT_INT = "int";
/**
- * A {@link SliceItem} that contains an int.
- * @deprecated to be removed
- */
- public static final String FORMAT_COLOR = "color";
- /**
* A {@link SliceItem} that contains a timestamp.
*/
public static final String FORMAT_TIMESTAMP = "timestamp";
@@ -231,13 +226,6 @@
}
/**
- * @deprecated to be removed.
- */
- public int getColor() {
- return (Integer) mObj;
- }
-
- /**
* @return The slice held by this {@link #FORMAT_ACTION} or {@link #FORMAT_SLICE} SliceItem
*/
public Slice getSlice() {
diff --git a/android/app/slice/SliceManager.java b/android/app/slice/SliceManager.java
index 0c5f225..2fa9d8e 100644
--- a/android/app/slice/SliceManager.java
+++ b/android/app/slice/SliceManager.java
@@ -16,18 +16,31 @@
package android.app.slice;
+import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemService;
+import android.content.ContentResolver;
import android.content.Context;
+import android.content.IContentProvider;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
import android.net.Uri;
+import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ServiceManager.ServiceNotFoundException;
import android.util.ArrayMap;
+import android.util.Log;
import android.util.Pair;
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
@@ -39,12 +52,36 @@
@SystemService(Context.SLICE_SERVICE)
public class SliceManager {
+ private static final String TAG = "SliceManager";
+
+ /**
+ * @hide
+ */
+ public static final String ACTION_REQUEST_SLICE_PERMISSION =
+ "android.intent.action.REQUEST_SLICE_PERMISSION";
+
private final ISliceManager mService;
private final Context mContext;
private final ArrayMap<Pair<Uri, SliceCallback>, ISliceListener> mListenerLookup =
new ArrayMap<>();
/**
+ * Permission denied.
+ * @hide
+ */
+ public static final int PERMISSION_DENIED = -1;
+ /**
+ * Permission granted.
+ * @hide
+ */
+ public static final int PERMISSION_GRANTED = 0;
+ /**
+ * Permission just granted by the user, and should be granted uri permission as well.
+ * @hide
+ */
+ public static final int PERMISSION_USER_GRANTED = 1;
+
+ /**
* @hide
*/
public SliceManager(Context context, Handler handler) throws ServiceNotFoundException {
@@ -54,66 +91,56 @@
}
/**
- * Adds a callback to a specific slice uri.
- * <p>
- * This is a convenience that performs a few slice actions at once. It will put
- * the slice in a pinned state since there is a callback attached. It will also
- * listen for content changes, when a content change observes, the android system
- * will bind the new slice and provide it to all registered {@link SliceCallback}s.
- *
- * @param uri The uri of the slice being listened to.
- * @param callback The listener that should receive the callbacks.
- * @param specs The list of supported {@link SliceSpec}s of the callback.
- * @see SliceProvider#onSlicePinned(Uri)
+ * @deprecated TO BE REMOVED.
*/
+ @Deprecated
public void registerSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback,
@NonNull List<SliceSpec> specs) {
- registerSliceCallback(uri, callback, specs, Handler.getMain());
+ registerSliceCallback(uri, specs, mContext.getMainExecutor(), callback);
}
/**
- * Adds a callback to a specific slice uri.
- * <p>
- * This is a convenience that performs a few slice actions at once. It will put
- * the slice in a pinned state since there is a callback attached. It will also
- * listen for content changes, when a content change observes, the android system
- * will bind the new slice and provide it to all registered {@link SliceCallback}s.
- *
- * @param uri The uri of the slice being listened to.
- * @param callback The listener that should receive the callbacks.
- * @param specs The list of supported {@link SliceSpec}s of the callback.
- * @see SliceProvider#onSlicePinned(Uri)
+ * @deprecated TO BE REMOVED.
*/
- public void registerSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback,
- @NonNull List<SliceSpec> specs, Handler handler) {
- try {
- mService.addSliceListener(uri, mContext.getPackageName(),
- getListener(uri, callback, new ISliceListener.Stub() {
- @Override
- public void onSliceUpdated(Slice s) throws RemoteException {
- handler.post(() -> callback.onSliceUpdated(s));
- }
- }), specs.toArray(new SliceSpec[specs.size()]));
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /**
- * Adds a callback to a specific slice uri.
- * <p>
- * This is a convenience that performs a few slice actions at once. It will put
- * the slice in a pinned state since there is a callback attached. It will also
- * listen for content changes, when a content change observes, the android system
- * will bind the new slice and provide it to all registered {@link SliceCallback}s.
- *
- * @param uri The uri of the slice being listened to.
- * @param callback The listener that should receive the callbacks.
- * @param specs The list of supported {@link SliceSpec}s of the callback.
- * @see SliceProvider#onSlicePinned(Uri)
- */
+ @Deprecated
public void registerSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback,
@NonNull List<SliceSpec> specs, Executor executor) {
+ registerSliceCallback(uri, specs, executor, callback);
+ }
+
+ /**
+ * Adds a callback to a specific slice uri.
+ * <p>
+ * This is a convenience that performs a few slice actions at once. It will put
+ * the slice in a pinned state since there is a callback attached. It will also
+ * listen for content changes, when a content change observes, the android system
+ * will bind the new slice and provide it to all registered {@link SliceCallback}s.
+ *
+ * @param uri The uri of the slice being listened to.
+ * @param callback The listener that should receive the callbacks.
+ * @param specs The list of supported {@link SliceSpec}s of the callback.
+ * @see SliceProvider#onSlicePinned(Uri)
+ */
+ public void registerSliceCallback(@NonNull Uri uri, @NonNull List<SliceSpec> specs,
+ @NonNull SliceCallback callback) {
+ registerSliceCallback(uri, specs, mContext.getMainExecutor(), callback);
+ }
+
+ /**
+ * Adds a callback to a specific slice uri.
+ * <p>
+ * This is a convenience that performs a few slice actions at once. It will put
+ * the slice in a pinned state since there is a callback attached. It will also
+ * listen for content changes, when a content change observes, the android system
+ * will bind the new slice and provide it to all registered {@link SliceCallback}s.
+ *
+ * @param uri The uri of the slice being listened to.
+ * @param callback The listener that should receive the callbacks.
+ * @param specs The list of supported {@link SliceSpec}s of the callback.
+ * @see SliceProvider#onSlicePinned(Uri)
+ */
+ public void registerSliceCallback(@NonNull Uri uri, @NonNull List<SliceSpec> specs,
+ @NonNull @CallbackExecutor Executor executor, @NonNull SliceCallback callback) {
try {
mService.addSliceListener(uri, mContext.getPackageName(),
getListener(uri, callback, new ISliceListener.Stub() {
@@ -224,6 +251,165 @@
}
/**
+ * Obtains a list of slices that are descendants of the specified Uri.
+ * <p>
+ * Not all slice providers will implement this functionality, in which case,
+ * an empty collection will be returned.
+ *
+ * @param uri The uri to look for descendants under.
+ * @return All slices within the space.
+ * @see SliceProvider#onGetSliceDescendants(Uri)
+ */
+ public @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri) {
+ ContentResolver resolver = mContext.getContentResolver();
+ IContentProvider provider = resolver.acquireProvider(uri);
+ try {
+ Bundle extras = new Bundle();
+ extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
+ final Bundle res = provider.call(resolver.getPackageName(),
+ SliceProvider.METHOD_GET_DESCENDANTS, null, extras);
+ return res.getParcelableArrayList(SliceProvider.EXTRA_SLICE_DESCENDANTS);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to get slice descendants", e);
+ } finally {
+ resolver.releaseProvider(provider);
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Turns a slice Uri into slice content.
+ *
+ * @param uri The URI to a slice provider
+ * @param supportedSpecs List of supported specs.
+ * @return The Slice provided by the app or null if none is given.
+ * @see Slice
+ */
+ public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
+ Preconditions.checkNotNull(uri, "uri");
+ ContentResolver resolver = mContext.getContentResolver();
+ IContentProvider provider = resolver.acquireProvider(uri);
+ if (provider == null) {
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ try {
+ Bundle extras = new Bundle();
+ extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
+ extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
+ new ArrayList<>(supportedSpecs));
+ final Bundle res = provider.call(mContext.getPackageName(), SliceProvider.METHOD_SLICE,
+ null, extras);
+ Bundle.setDefusable(res, true);
+ if (res == null) {
+ return null;
+ }
+ return res.getParcelable(SliceProvider.EXTRA_SLICE);
+ } catch (RemoteException e) {
+ // Arbitrary and not worth documenting, as Activity
+ // Manager will kill this process shortly anyway.
+ return null;
+ } finally {
+ resolver.releaseProvider(provider);
+ }
+ }
+
+ /**
+ * Turns a slice intent into slice content. Expects an explicit intent. If there is no
+ * {@link android.content.ContentProvider} associated with the given intent this will throw
+ * {@link IllegalArgumentException}.
+ *
+ * @param intent The intent associated with a slice.
+ * @param supportedSpecs List of supported specs.
+ * @return The Slice provided by the app or null if none is given.
+ * @see Slice
+ * @see SliceProvider#onMapIntentToUri(Intent)
+ * @see Intent
+ */
+ public @Nullable Slice bindSlice(@NonNull Intent intent,
+ @NonNull List<SliceSpec> supportedSpecs) {
+ Preconditions.checkNotNull(intent, "intent");
+ Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null,
+ "Slice intent must be explicit " + intent);
+ ContentResolver resolver = mContext.getContentResolver();
+
+ // Check if the intent has data for the slice uri on it and use that
+ final Uri intentData = intent.getData();
+ if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
+ return bindSlice(intentData, supportedSpecs);
+ }
+ // Otherwise ask the app
+ List<ResolveInfo> providers =
+ mContext.getPackageManager().queryIntentContentProviders(intent, 0);
+ if (providers == null) {
+ throw new IllegalArgumentException("Unable to resolve intent " + intent);
+ }
+ String authority = providers.get(0).providerInfo.authority;
+ Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(authority).build();
+ IContentProvider provider = resolver.acquireProvider(uri);
+ if (provider == null) {
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ try {
+ Bundle extras = new Bundle();
+ extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
+ extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
+ new ArrayList<>(supportedSpecs));
+ final Bundle res = provider.call(mContext.getPackageName(),
+ SliceProvider.METHOD_MAP_INTENT, null, extras);
+ if (res == null) {
+ return null;
+ }
+ return res.getParcelable(SliceProvider.EXTRA_SLICE);
+ } catch (RemoteException e) {
+ // Arbitrary and not worth documenting, as Activity
+ // Manager will kill this process shortly anyway.
+ return null;
+ } finally {
+ resolver.releaseProvider(provider);
+ }
+ }
+
+ /**
+ * Does the permission check to see if a caller has access to a specific slice.
+ * @hide
+ */
+ public void enforceSlicePermission(Uri uri, String pkg, int pid, int uid) {
+ try {
+ if (pkg == null) {
+ throw new SecurityException("No pkg specified");
+ }
+ int result = mService.checkSlicePermission(uri, pkg, pid, uid);
+ if (result == PERMISSION_DENIED) {
+ throw new SecurityException("User " + uid + " does not have slice permission for "
+ + uri + ".");
+ }
+ if (result == PERMISSION_USER_GRANTED) {
+ // We just had a user grant of this permission and need to grant this to the app
+ // permanently.
+ mContext.grantUriPermission(pkg, uri.buildUpon().path("").build(),
+ Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by SystemUI to grant a slice permission after a dialog is shown.
+ * @hide
+ */
+ public void grantPermissionFromUser(Uri uri, String pkg, boolean allSlices) {
+ try {
+ mService.grantPermissionFromUser(uri, pkg, mContext.getPackageName(), allSlices);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Class that listens to changes in {@link Slice}s.
*/
public interface SliceCallback {
diff --git a/android/app/slice/SliceProvider.java b/android/app/slice/SliceProvider.java
index 8483931..00e8cca 100644
--- a/android/app/slice/SliceProvider.java
+++ b/android/app/slice/SliceProvider.java
@@ -15,13 +15,19 @@
*/
package android.app.slice;
-import android.Manifest.permission;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
+import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ProviderInfo;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
@@ -36,6 +42,9 @@
import android.os.UserHandle;
import android.util.Log;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
@@ -80,7 +89,7 @@
*/
public abstract class SliceProvider extends ContentProvider {
/**
- * This is the Android platform's MIME type for a slice: URI
+ * This is the Android platform's MIME type for a URI
* containing a slice implemented through {@link SliceProvider}.
*/
public static final String SLICE_TYPE = "vnd.android.slice";
@@ -113,14 +122,53 @@
/**
* @hide
*/
+ public static final String METHOD_GET_DESCENDANTS = "get_descendants";
+ /**
+ * @hide
+ */
public static final String EXTRA_INTENT = "slice_intent";
/**
* @hide
*/
public static final String EXTRA_SLICE = "slice";
+ /**
+ * @hide
+ */
+ public static final String EXTRA_SLICE_DESCENDANTS = "slice_descendants";
+ /**
+ * @hide
+ */
+ public static final String EXTRA_PKG = "pkg";
+ /**
+ * @hide
+ */
+ public static final String EXTRA_PROVIDER_PKG = "provider_pkg";
+ /**
+ * @hide
+ */
+ public static final String EXTRA_OVERRIDE_PKG = "override_pkg";
private static final boolean DEBUG = false;
+ private String mBindingPkg;
+ private SliceManager mSliceManager;
+
+ /**
+ * Return the package name of the caller that initiated the binding request
+ * currently happening. The returned package will have been
+ * verified to belong to the calling UID. Returns {@code null} if not
+ * currently performing an {@link #onBindSlice(Uri, List)}.
+ */
+ public final @Nullable String getBindingPackage() {
+ return mBindingPkg;
+ }
+
+ @Override
+ public void attachInfo(Context context, ProviderInfo info) {
+ super.attachInfo(context, info);
+ mSliceManager = context.getSystemService(SliceManager.class);
+ }
+
/**
* Implemented to create a slice. Will be called on the main thread.
* <p>
@@ -139,14 +187,6 @@
* @see {@link Slice#HINT_PARTIAL}
*/
public Slice onBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs) {
- return onBindSlice(sliceUri);
- }
-
- /**
- * @deprecated migrating to {@link #onBindSlice(Uri, List)}
- */
- @Deprecated
- public Slice onBindSlice(Uri sliceUri) {
return null;
}
@@ -183,6 +223,20 @@
}
/**
+ * Obtains a list of slices that are descendants of the specified Uri.
+ * <p>
+ * Implementing this is optional for a SliceProvider, but does provide a good
+ * discovery mechanism for finding slice Uris.
+ *
+ * @param uri The uri to look for descendants under.
+ * @return All slices within the space.
+ * @see SliceManager#getSliceDescendants(Uri)
+ */
+ public @NonNull Collection<Uri> onGetSliceDescendants(@NonNull Uri uri) {
+ return Collections.emptyList();
+ }
+
+ /**
* This method must be overridden if an {@link IntentFilter} is specified on the SliceProvider.
* In that case, this method can be called and is expected to return a non-null Uri representing
* a slice. Otherwise this will throw {@link UnsupportedOperationException}.
@@ -244,56 +298,74 @@
@Override
public Bundle call(String method, String arg, Bundle extras) {
if (method.equals(METHOD_SLICE)) {
- Uri uri = extras.getParcelable(EXTRA_BIND_URI);
- if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) {
- getContext().enforceUriPermission(uri, permission.BIND_SLICE,
- permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(),
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
- "Slice binding requires the permission BIND_SLICE");
- }
+ Uri uri = getUriWithoutUserId(extras.getParcelable(EXTRA_BIND_URI));
List<SliceSpec> supportedSpecs = extras.getParcelableArrayList(EXTRA_SUPPORTED_SPECS);
- Slice s = handleBindSlice(uri, supportedSpecs);
+ String callingPackage = getCallingPackage();
+ if (extras.containsKey(EXTRA_OVERRIDE_PKG)) {
+ if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+ throw new SecurityException("Only the system can override calling pkg");
+ }
+ callingPackage = extras.getString(EXTRA_OVERRIDE_PKG);
+ }
+ Slice s = handleBindSlice(uri, supportedSpecs, callingPackage);
Bundle b = new Bundle();
b.putParcelable(EXTRA_SLICE, s);
return b;
} else if (method.equals(METHOD_MAP_INTENT)) {
- getContext().enforceCallingPermission(permission.BIND_SLICE,
- "Slice binding requires the permission BIND_SLICE");
Intent intent = extras.getParcelable(EXTRA_INTENT);
if (intent == null) return null;
Uri uri = onMapIntentToUri(intent);
List<SliceSpec> supportedSpecs = extras.getParcelableArrayList(EXTRA_SUPPORTED_SPECS);
Bundle b = new Bundle();
if (uri != null) {
- Slice s = handleBindSlice(uri, supportedSpecs);
+ Slice s = handleBindSlice(uri, supportedSpecs, getCallingPackage());
b.putParcelable(EXTRA_SLICE, s);
} else {
b.putParcelable(EXTRA_SLICE, null);
}
return b;
} else if (method.equals(METHOD_PIN)) {
- Uri uri = extras.getParcelable(EXTRA_BIND_URI);
- if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) {
- getContext().enforceUriPermission(uri, permission.BIND_SLICE,
- permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(),
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
- "Slice binding requires the permission BIND_SLICE");
+ Uri uri = getUriWithoutUserId(extras.getParcelable(EXTRA_BIND_URI));
+ if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+ throw new SecurityException("Only the system can pin/unpin slices");
}
handlePinSlice(uri);
} else if (method.equals(METHOD_UNPIN)) {
- Uri uri = extras.getParcelable(EXTRA_BIND_URI);
- if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) {
- getContext().enforceUriPermission(uri, permission.BIND_SLICE,
- permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(),
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
- "Slice binding requires the permission BIND_SLICE");
+ Uri uri = getUriWithoutUserId(extras.getParcelable(EXTRA_BIND_URI));
+ if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+ throw new SecurityException("Only the system can pin/unpin slices");
}
handleUnpinSlice(uri);
+ } else if (method.equals(METHOD_GET_DESCENDANTS)) {
+ Uri uri = getUriWithoutUserId(extras.getParcelable(EXTRA_BIND_URI));
+ Bundle b = new Bundle();
+ b.putParcelableArrayList(EXTRA_SLICE_DESCENDANTS,
+ new ArrayList<>(handleGetDescendants(uri)));
+ return b;
}
return super.call(method, arg, extras);
}
+ private Collection<Uri> handleGetDescendants(Uri uri) {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ return onGetSliceDescendants(uri);
+ } else {
+ CountDownLatch latch = new CountDownLatch(1);
+ Collection<Uri>[] output = new Collection[1];
+ Handler.getMain().post(() -> {
+ output[0] = onGetSliceDescendants(uri);
+ latch.countDown();
+ });
+ try {
+ latch.await();
+ return output[0];
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
private void handlePinSlice(Uri sliceUri) {
if (Looper.myLooper() == Looper.getMainLooper()) {
onSlicePinned(sliceUri);
@@ -328,14 +400,27 @@
}
}
- private Slice handleBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs) {
+ private Slice handleBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs,
+ String callingPkg) {
+ // This can be removed once Slice#bindSlice is removed and everyone is using
+ // SliceManager#bindSlice.
+ String pkg = callingPkg != null ? callingPkg
+ : getContext().getPackageManager().getNameForUid(Binder.getCallingUid());
+ if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) {
+ try {
+ mSliceManager.enforceSlicePermission(sliceUri, pkg,
+ Binder.getCallingPid(), Binder.getCallingUid());
+ } catch (SecurityException e) {
+ return createPermissionSlice(getContext(), sliceUri, pkg);
+ }
+ }
if (Looper.myLooper() == Looper.getMainLooper()) {
- return onBindSliceStrict(sliceUri, supportedSpecs);
+ return onBindSliceStrict(sliceUri, supportedSpecs, pkg);
} else {
CountDownLatch latch = new CountDownLatch(1);
Slice[] output = new Slice[1];
Handler.getMain().post(() -> {
- output[0] = onBindSliceStrict(sliceUri, supportedSpecs);
+ output[0] = onBindSliceStrict(sliceUri, supportedSpecs, pkg);
latch.countDown();
});
try {
@@ -347,15 +432,66 @@
}
}
- private Slice onBindSliceStrict(Uri sliceUri, List<SliceSpec> supportedSpecs) {
+ /**
+ * @hide
+ */
+ public static Slice createPermissionSlice(Context context, Uri sliceUri,
+ String callingPackage) {
+ return new Slice.Builder(sliceUri)
+ .addAction(createPermissionIntent(context, sliceUri, callingPackage),
+ new Slice.Builder(sliceUri.buildUpon().appendPath("permission").build())
+ .addText(getPermissionString(context, callingPackage), null)
+ .build())
+ .addHints(Slice.HINT_LIST_ITEM)
+ .build();
+ }
+
+ /**
+ * @hide
+ */
+ public static PendingIntent createPermissionIntent(Context context, Uri sliceUri,
+ String callingPackage) {
+ Intent intent = new Intent(SliceManager.ACTION_REQUEST_SLICE_PERMISSION);
+ intent.setComponent(new ComponentName("com.android.systemui",
+ "com.android.systemui.SlicePermissionActivity"));
+ intent.putExtra(EXTRA_BIND_URI, sliceUri);
+ intent.putExtra(EXTRA_PKG, callingPackage);
+ intent.putExtra(EXTRA_PROVIDER_PKG, context.getPackageName());
+ // Unique pending intent.
+ intent.setData(sliceUri.buildUpon().appendQueryParameter("package", callingPackage)
+ .build());
+
+ return PendingIntent.getActivity(context, 0, intent, 0);
+ }
+
+ /**
+ * @hide
+ */
+ public static CharSequence getPermissionString(Context context, String callingPackage) {
+ PackageManager pm = context.getPackageManager();
+ try {
+ return context.getString(
+ com.android.internal.R.string.slices_permission_request,
+ pm.getApplicationInfo(callingPackage, 0).loadLabel(pm),
+ context.getApplicationInfo().loadLabel(pm));
+ } catch (NameNotFoundException e) {
+ // This shouldn't be possible since the caller is verified.
+ throw new RuntimeException("Unknown calling app", e);
+ }
+ }
+
+ private Slice onBindSliceStrict(Uri sliceUri, List<SliceSpec> supportedSpecs,
+ String callingPackage) {
ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
try {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDeath()
.build());
+ mBindingPkg = callingPackage;
return onBindSlice(sliceUri, supportedSpecs);
} finally {
+ mBindingPkg = null;
StrictMode.setThreadPolicy(oldPolicy);
}
}
diff --git a/android/app/timezone/RulesState.java b/android/app/timezone/RulesState.java
index 16309fa..e86d348 100644
--- a/android/app/timezone/RulesState.java
+++ b/android/app/timezone/RulesState.java
@@ -126,9 +126,6 @@
mStagedOperationType == STAGED_OPERATION_INSTALL /* requireNotNull */,
"stagedDistroRulesVersion", stagedDistroRulesVersion);
- if (operationInProgress && distroStatus != DISTRO_STATUS_UNKNOWN) {
- throw new IllegalArgumentException("distroInstalled != DISTRO_STATUS_UNKNOWN");
- }
this.mDistroStatus = validateDistroStatus(distroStatus);
this.mInstalledDistroRulesVersion = validateConditionalNull(
mDistroStatus == DISTRO_STATUS_INSTALLED/* requireNotNull */,
diff --git a/android/app/trust/TrustManager.java b/android/app/trust/TrustManager.java
index 852cb8e..8ab0b70 100644
--- a/android/app/trust/TrustManager.java
+++ b/android/app/trust/TrustManager.java
@@ -36,9 +36,11 @@
private static final int MSG_TRUST_CHANGED = 1;
private static final int MSG_TRUST_MANAGED_CHANGED = 2;
+ private static final int MSG_TRUST_ERROR = 3;
private static final String TAG = "TrustManager";
private static final String DATA_FLAGS = "initiatedByUser";
+ private static final String DATA_MESSAGE = "message";
private final ITrustManager mService;
private final ArrayMap<TrustListener, ITrustListener> mTrustListeners;
@@ -148,6 +150,13 @@
mHandler.obtainMessage(MSG_TRUST_MANAGED_CHANGED, (managed ? 1 : 0), userId,
trustListener).sendToTarget();
}
+
+ @Override
+ public void onTrustError(CharSequence message) {
+ Message m = mHandler.obtainMessage(MSG_TRUST_ERROR);
+ m.getData().putCharSequence(DATA_MESSAGE, message);
+ m.sendToTarget();
+ }
};
mService.registerTrustListener(iTrustListener);
mTrustListeners.put(trustListener, iTrustListener);
@@ -221,6 +230,10 @@
break;
case MSG_TRUST_MANAGED_CHANGED:
((TrustListener)msg.obj).onTrustManagedChanged(msg.arg1 != 0, msg.arg2);
+ break;
+ case MSG_TRUST_ERROR:
+ final CharSequence message = msg.peekData().getCharSequence(DATA_MESSAGE);
+ ((TrustListener)msg.obj).onTrustError(message);
}
}
};
@@ -229,9 +242,9 @@
/**
* Reports that the trust state has changed.
- * @param enabled if true, the system believes the environment to be trusted.
- * @param userId the user, for which the trust changed.
- * @param flags flags specified by the trust agent when granting trust. See
+ * @param enabled If true, the system believes the environment to be trusted.
+ * @param userId The user, for which the trust changed.
+ * @param flags Flags specified by the trust agent when granting trust. See
* {@link android.service.trust.TrustAgentService#grantTrust(CharSequence, long, int)
* TrustAgentService.grantTrust(CharSequence, long, int)}.
*/
@@ -239,9 +252,15 @@
/**
* Reports that whether trust is managed has changed
- * @param enabled if true, at least one trust agent is managing trust.
- * @param userId the user, for which the state changed.
+ * @param enabled If true, at least one trust agent is managing trust.
+ * @param userId The user, for which the state changed.
*/
void onTrustManagedChanged(boolean enabled, int userId);
+
+ /**
+ * Reports that an error happened on a TrustAgentService.
+ * @param message A message that should be displayed on the UI.
+ */
+ void onTrustError(CharSequence message);
}
}
diff --git a/android/app/usage/NetworkStats.java b/android/app/usage/NetworkStats.java
index 2e44a63..da36157 100644
--- a/android/app/usage/NetworkStats.java
+++ b/android/app/usage/NetworkStats.java
@@ -227,6 +227,30 @@
*/
public static final int ROAMING_YES = 0x2;
+ /** @hide */
+ @IntDef(prefix = { "DEFAULT_NETWORK_" }, value = {
+ DEFAULT_NETWORK_ALL,
+ DEFAULT_NETWORK_NO,
+ DEFAULT_NETWORK_YES
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DefaultNetwork {}
+
+ /**
+ * Combined usage for this network regardless of whether it was the active default network.
+ */
+ public static final int DEFAULT_NETWORK_ALL = -1;
+
+ /**
+ * Usage that occurs while this network is not the active default network.
+ */
+ public static final int DEFAULT_NETWORK_NO = 0x1;
+
+ /**
+ * Usage that occurs while this network is the active default network.
+ */
+ public static final int DEFAULT_NETWORK_YES = 0x2;
+
/**
* Special TAG value for total data across all tags
*/
@@ -235,6 +259,7 @@
private int mUid;
private int mTag;
private int mState;
+ private int mDefaultNetwork;
private int mMetered;
private int mRoaming;
private long mBeginTimeStamp;
@@ -286,6 +311,15 @@
return 0;
}
+ private static @DefaultNetwork int convertDefaultNetwork(int defaultNetwork) {
+ switch (defaultNetwork) {
+ case android.net.NetworkStats.DEFAULT_NETWORK_ALL : return DEFAULT_NETWORK_ALL;
+ case android.net.NetworkStats.DEFAULT_NETWORK_NO: return DEFAULT_NETWORK_NO;
+ case android.net.NetworkStats.DEFAULT_NETWORK_YES: return DEFAULT_NETWORK_YES;
+ }
+ return 0;
+ }
+
public Bucket() {
}
@@ -351,6 +385,21 @@
}
/**
+ * Default network state. One of the following values:<p/>
+ * <ul>
+ * <li>{@link #DEFAULT_NETWORK_ALL}</li>
+ * <li>{@link #DEFAULT_NETWORK_NO}</li>
+ * <li>{@link #DEFAULT_NETWORK_YES}</li>
+ * </ul>
+ * <p>Indicates whether the network usage occurred on the system default network for this
+ * type of traffic, or whether the application chose to send this traffic on a network that
+ * was not the one selected by the system.
+ */
+ public @DefaultNetwork int getDefaultNetwork() {
+ return mDefaultNetwork;
+ }
+
+ /**
* Start timestamp of the bucket's time interval. Defined in terms of "Unix time", see
* {@link java.lang.System#currentTimeMillis}.
* @return Start of interval.
@@ -551,6 +600,8 @@
bucketOut.mUid = Bucket.convertUid(mRecycledSummaryEntry.uid);
bucketOut.mTag = Bucket.convertTag(mRecycledSummaryEntry.tag);
bucketOut.mState = Bucket.convertState(mRecycledSummaryEntry.set);
+ bucketOut.mDefaultNetwork = Bucket.convertDefaultNetwork(
+ mRecycledSummaryEntry.defaultNetwork);
bucketOut.mMetered = Bucket.convertMetered(mRecycledSummaryEntry.metered);
bucketOut.mRoaming = Bucket.convertRoaming(mRecycledSummaryEntry.roaming);
bucketOut.mBeginTimeStamp = mStartTimeStamp;
@@ -600,6 +651,7 @@
bucketOut.mUid = Bucket.convertUid(getUid());
bucketOut.mTag = Bucket.convertTag(mTag);
bucketOut.mState = Bucket.STATE_ALL;
+ bucketOut.mDefaultNetwork = Bucket.DEFAULT_NETWORK_ALL;
bucketOut.mMetered = Bucket.METERED_ALL;
bucketOut.mRoaming = Bucket.ROAMING_ALL;
bucketOut.mBeginTimeStamp = mRecycledHistoryEntry.bucketStart;
diff --git a/android/app/usage/NetworkStatsManager.java b/android/app/usage/NetworkStatsManager.java
index 853b003..5576e86 100644
--- a/android/app/usage/NetworkStatsManager.java
+++ b/android/app/usage/NetworkStatsManager.java
@@ -60,10 +60,11 @@
* {@link #queryDetailsForUid} <p />
* {@link #queryDetails} <p />
* These queries do not aggregate over time but do aggregate over state, metered and roaming.
- * Therefore there can be multiple buckets for a particular key but all Bucket's state is going to
- * be {@link NetworkStats.Bucket#STATE_ALL}, all Bucket's metered is going to be
- * {@link NetworkStats.Bucket#METERED_ALL}, and all Bucket's roaming is going to be
- * {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * Therefore there can be multiple buckets for a particular key. However, all Buckets will have
+ * {@code state} {@link NetworkStats.Bucket#STATE_ALL},
+ * {@code defaultNetwork} {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * {@code metered } {@link NetworkStats.Bucket#METERED_ALL},
+ * {@code roaming} {@link NetworkStats.Bucket#ROAMING_ALL}.
* <p />
* <b>NOTE:</b> Calling {@link #querySummaryForDevice} or accessing stats for apps other than the
* calling app requires the permission {@link android.Manifest.permission#PACKAGE_USAGE_STATS},
@@ -130,13 +131,26 @@
}
}
+ /** @hide */
+ public Bucket querySummaryForDevice(NetworkTemplate template,
+ long startTime, long endTime) throws SecurityException, RemoteException {
+ Bucket bucket = null;
+ NetworkStats stats = new NetworkStats(mContext, template, mFlags, startTime, endTime);
+ bucket = stats.getDeviceSummaryForNetwork();
+
+ stats.close();
+ return bucket;
+ }
+
/**
* Query network usage statistics summaries. Result is summarised data usage for the whole
* device. Result is a single Bucket aggregated over time, state, uid, tag, metered, and
* roaming. This means the bucket's start and end timestamp are going to be the same as the
* 'startTime' and 'endTime' parameters. State is going to be
* {@link NetworkStats.Bucket#STATE_ALL}, uid {@link NetworkStats.Bucket#UID_ALL},
- * tag {@link NetworkStats.Bucket#TAG_NONE}, metered {@link NetworkStats.Bucket#METERED_ALL},
+ * tag {@link NetworkStats.Bucket#TAG_NONE},
+ * default network {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * metered {@link NetworkStats.Bucket#METERED_ALL},
* and roaming {@link NetworkStats.Bucket#ROAMING_ALL}.
*
* @param networkType As defined in {@link ConnectivityManager}, e.g.
@@ -160,12 +174,7 @@
return null;
}
- Bucket bucket = null;
- NetworkStats stats = new NetworkStats(mContext, template, mFlags, startTime, endTime);
- bucket = stats.getDeviceSummaryForNetwork();
-
- stats.close();
- return bucket;
+ return querySummaryForDevice(template, startTime, endTime);
}
/**
@@ -209,10 +218,10 @@
/**
* Query network usage statistics summaries. Result filtered to include only uids belonging to
* calling user. Result is aggregated over time, hence all buckets will have the same start and
- * end timestamps. Not aggregated over state, uid, metered, or roaming. This means buckets'
- * start and end timestamps are going to be the same as the 'startTime' and 'endTime'
- * parameters. State, uid, metered, and roaming are going to vary, and tag is going to be the
- * same.
+ * end timestamps. Not aggregated over state, uid, default network, metered, or roaming. This
+ * means buckets' start and end timestamps are going to be the same as the 'startTime' and
+ * 'endTime' parameters. State, uid, metered, and roaming are going to vary, and tag is going to
+ * be the same.
*
* @param networkType As defined in {@link ConnectivityManager}, e.g.
* {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
@@ -258,9 +267,10 @@
* belonging to calling user. Result is aggregated over state but not aggregated over time.
* This means buckets' start and end timestamps are going to be between 'startTime' and
* 'endTime' parameters. State is going to be {@link NetworkStats.Bucket#STATE_ALL}, uid the
- * same as the 'uid' parameter and tag the same as 'tag' parameter. metered is going to be
- * {@link NetworkStats.Bucket#METERED_ALL}, and roaming is going to be
- * {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * same as the 'uid' parameter and tag the same as 'tag' parameter.
+ * defaultNetwork is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, and
+ * roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
* <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
* interpolate across partial buckets. Since bucket length is in the order of hours, this
* method cannot be used to measure data usage on a fine grained time scale.
@@ -301,9 +311,10 @@
* metered, nor roaming. This means buckets' start and end timestamps are going to be between
* 'startTime' and 'endTime' parameters. State is going to be
* {@link NetworkStats.Bucket#STATE_ALL}, uid will vary,
- * tag {@link NetworkStats.Bucket#TAG_NONE}, metered is going to be
- * {@link NetworkStats.Bucket#METERED_ALL}, and roaming is going to be
- * {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * tag {@link NetworkStats.Bucket#TAG_NONE},
+ * default network is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * metered is going to be {@link NetworkStats.Bucket#METERED_ALL},
+ * and roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
* <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
* interpolate across partial buckets. Since bucket length is in the order of hours, this
* method cannot be used to measure data usage on a fine grained time scale.
@@ -335,6 +346,37 @@
return result;
}
+ /** @hide */
+ public void registerUsageCallback(NetworkTemplate template, int networkType,
+ long thresholdBytes, UsageCallback callback, @Nullable Handler handler) {
+ checkNotNull(callback, "UsageCallback cannot be null");
+
+ final Looper looper;
+ if (handler == null) {
+ looper = Looper.myLooper();
+ } else {
+ looper = handler.getLooper();
+ }
+
+ DataUsageRequest request = new DataUsageRequest(DataUsageRequest.REQUEST_ID_UNSET,
+ template, thresholdBytes);
+ try {
+ CallbackHandler callbackHandler = new CallbackHandler(looper, networkType,
+ template.getSubscriberId(), callback);
+ callback.request = mService.registerUsageCallback(
+ mContext.getOpPackageName(), request, new Messenger(callbackHandler),
+ new Binder());
+ if (DBG) Log.d(TAG, "registerUsageCallback returned " + callback.request);
+
+ if (callback.request == null) {
+ Log.e(TAG, "Request from callback is null; should not happen");
+ }
+ } catch (RemoteException e) {
+ if (DBG) Log.d(TAG, "Remote exception when registering callback");
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
/**
* Registers to receive notifications about data usage on specified networks.
*
@@ -363,15 +405,7 @@
*/
public void registerUsageCallback(int networkType, String subscriberId, long thresholdBytes,
UsageCallback callback, @Nullable Handler handler) {
- checkNotNull(callback, "UsageCallback cannot be null");
-
- final Looper looper;
- if (handler == null) {
- looper = Looper.myLooper();
- } else {
- looper = handler.getLooper();
- }
-
+ NetworkTemplate template = createTemplate(networkType, subscriberId);
if (DBG) {
Log.d(TAG, "registerUsageCallback called with: {"
+ " networkType=" + networkType
@@ -379,25 +413,7 @@
+ " thresholdBytes=" + thresholdBytes
+ " }");
}
-
- NetworkTemplate template = createTemplate(networkType, subscriberId);
- DataUsageRequest request = new DataUsageRequest(DataUsageRequest.REQUEST_ID_UNSET,
- template, thresholdBytes);
- try {
- CallbackHandler callbackHandler = new CallbackHandler(looper, networkType,
- subscriberId, callback);
- callback.request = mService.registerUsageCallback(
- mContext.getOpPackageName(), request, new Messenger(callbackHandler),
- new Binder());
- if (DBG) Log.d(TAG, "registerUsageCallback returned " + callback.request);
-
- if (callback.request == null) {
- Log.e(TAG, "Request from callback is null; should not happen");
- }
- } catch (RemoteException e) {
- if (DBG) Log.d(TAG, "Remote exception when registering callback");
- throw e.rethrowFromSystemServer();
- }
+ registerUsageCallback(template, networkType, thresholdBytes, callback, handler);
}
/**
diff --git a/android/app/usage/UsageEvents.java b/android/app/usage/UsageEvents.java
index f04e907..edb992b 100644
--- a/android/app/usage/UsageEvents.java
+++ b/android/app/usage/UsageEvents.java
@@ -106,6 +106,12 @@
*/
public static final int NOTIFICATION_SEEN = 10;
+ /**
+ * An event type denoting a change in App Standby Bucket.
+ * @hide
+ */
+ public static final int STANDBY_BUCKET_CHANGED = 11;
+
/** @hide */
public static final int FLAG_IS_PACKAGE_INSTANT_APP = 1 << 0;
@@ -170,6 +176,13 @@
*/
public String[] mContentAnnotations;
+ /**
+ * The app standby bucket assigned.
+ * Only present for {@link #STANDBY_BUCKET_CHANGED} event types
+ * {@hide}
+ */
+ public int mBucket;
+
/** @hide */
@EventFlags
public int mFlags;
@@ -189,6 +202,7 @@
mContentType = orig.mContentType;
mContentAnnotations = orig.mContentAnnotations;
mFlags = orig.mFlags;
+ mBucket = orig.mBucket;
}
/**
@@ -399,6 +413,9 @@
p.writeString(event.mContentType);
p.writeStringArray(event.mContentAnnotations);
break;
+ case Event.STANDBY_BUCKET_CHANGED:
+ p.writeInt(event.mBucket);
+ break;
}
}
@@ -442,6 +459,9 @@
eventOut.mContentType = p.readString();
eventOut.mContentAnnotations = p.createStringArray();
break;
+ case Event.STANDBY_BUCKET_CHANGED:
+ eventOut.mBucket = p.readInt();
+ break;
}
}
diff --git a/android/app/usage/UsageStatsManagerInternal.java b/android/app/usage/UsageStatsManagerInternal.java
index 4b4fe72..bd978e3 100644
--- a/android/app/usage/UsageStatsManagerInternal.java
+++ b/android/app/usage/UsageStatsManagerInternal.java
@@ -16,11 +16,13 @@
package android.app.usage;
+import android.annotation.UserIdInt;
import android.app.usage.UsageStatsManager.StandbyBuckets;
import android.content.ComponentName;
import android.content.res.Configuration;
import java.util.List;
+import java.util.Set;
/**
* UsageStatsManager local system service interface.
@@ -37,7 +39,7 @@
* @param eventType The event that occurred. Valid values can be found at
* {@link UsageEvents}
*/
- public abstract void reportEvent(ComponentName component, int userId, int eventType);
+ public abstract void reportEvent(ComponentName component, @UserIdInt int userId, int eventType);
/**
* Reports an event to the UsageStatsManager.
@@ -47,14 +49,14 @@
* @param eventType The event that occurred. Valid values can be found at
* {@link UsageEvents}
*/
- public abstract void reportEvent(String packageName, int userId, int eventType);
+ public abstract void reportEvent(String packageName, @UserIdInt int userId, int eventType);
/**
* Reports a configuration change to the UsageStatsManager.
*
* @param config The new device configuration.
*/
- public abstract void reportConfigurationChange(Configuration config, int userId);
+ public abstract void reportConfigurationChange(Configuration config, @UserIdInt int userId);
/**
* Reports that an action equivalent to a ShortcutInfo is taken by the user.
@@ -65,7 +67,8 @@
*
* @see android.content.pm.ShortcutManager#reportShortcutUsed(String)
*/
- public abstract void reportShortcutUsage(String packageName, String shortcutId, int userId);
+ public abstract void reportShortcutUsage(String packageName, String shortcutId,
+ @UserIdInt int userId);
/**
* Reports that a content provider has been accessed by a foreground app.
@@ -73,7 +76,8 @@
* @param pkgName The package name of the content provider
* @param userId The user in which the content provider was accessed.
*/
- public abstract void reportContentProviderUsage(String name, String pkgName, int userId);
+ public abstract void reportContentProviderUsage(String name, String pkgName,
+ @UserIdInt int userId);
/**
* Prepares the UsageStatsService for shutdown.
@@ -89,7 +93,7 @@
* @param userId
* @return
*/
- public abstract boolean isAppIdle(String packageName, int uidForAppId, int userId);
+ public abstract boolean isAppIdle(String packageName, int uidForAppId, @UserIdInt int userId);
/**
* Returns the app standby bucket that the app is currently in. This accessor does
@@ -101,15 +105,15 @@
* @return the AppStandby bucket code the app currently resides in. If the app is
* unknown in the given user, STANDBY_BUCKET_NEVER is returned.
*/
- @StandbyBuckets public abstract int getAppStandbyBucket(String packageName, int userId,
- long nowElapsed);
+ @StandbyBuckets public abstract int getAppStandbyBucket(String packageName,
+ @UserIdInt int userId, long nowElapsed);
/**
* Returns all of the uids for a given user where all packages associating with that uid
* are in the app idle state -- there are no associated apps that are not idle. This means
* all of the returned uids can be safely considered app idle.
*/
- public abstract int[] getIdleUidsForUser(int userId);
+ public abstract int[] getIdleUidsForUser(@UserIdInt int userId);
/**
* @return True if currently app idle parole mode is on. This means all idle apps are allow to
@@ -134,8 +138,8 @@
public static abstract class AppIdleStateChangeListener {
/** Callback to inform listeners that the idle state has changed to a new bucket. */
- public abstract void onAppIdleStateChanged(String packageName, int userId, boolean idle,
- int bucket);
+ public abstract void onAppIdleStateChanged(String packageName, @UserIdInt int userId,
+ boolean idle, int bucket);
/**
* Callback to inform listeners that the parole state has changed. This means apps are
@@ -144,10 +148,38 @@
public abstract void onParoleStateChanged(boolean isParoleOn);
}
- /* Backup/Restore API */
- public abstract byte[] getBackupPayload(int user, String key);
+ /** Backup/Restore API */
+ public abstract byte[] getBackupPayload(@UserIdInt int userId, String key);
- public abstract void applyRestoredPayload(int user, String key, byte[] payload);
+ /**
+ * ?
+ * @param userId
+ * @param key
+ * @param payload
+ */
+ public abstract void applyRestoredPayload(@UserIdInt int userId, String key, byte[] payload);
+
+ /**
+ * Called by DevicePolicyManagerService to inform that a new admin has been added.
+ *
+ * @param packageName the package in which the admin component is part of.
+ * @param userId the userId in which the admin has been added.
+ */
+ public abstract void onActiveAdminAdded(String packageName, int userId);
+
+ /**
+ * Called by DevicePolicyManagerService to inform about the active admins in an user.
+ *
+ * @param adminApps the set of active admins in {@param userId} or null if there are none.
+ * @param userId the userId to which the admin apps belong.
+ */
+ public abstract void setActiveAdminApps(Set<String> adminApps, int userId);
+
+ /**
+ * Called by DevicePolicyManagerService during boot to inform that admin data is loaded and
+ * pushed to UsageStatsService.
+ */
+ public abstract void onAdminDataAvailable();
/**
* Return usage stats.
@@ -155,6 +187,29 @@
* @param obfuscateInstantApps whether instant app package names need to be obfuscated in the
* result.
*/
- public abstract List<UsageStats> queryUsageStatsForUser(
- int userId, int interval, long beginTime, long endTime, boolean obfuscateInstantApps);
+ public abstract List<UsageStats> queryUsageStatsForUser(@UserIdInt int userId, int interval,
+ long beginTime, long endTime, boolean obfuscateInstantApps);
+
+ /**
+ * Used to persist the last time a job was run for this app, in order to make decisions later
+ * whether a job should be deferred until later. The time passed in should be in elapsed
+ * realtime since boot.
+ * @param packageName the app that executed a job.
+ * @param userId the user associated with the job.
+ * @param elapsedRealtime the time when the job was executed, in elapsed realtime millis since
+ * boot.
+ */
+ public abstract void setLastJobRunTime(String packageName, @UserIdInt int userId,
+ long elapsedRealtime);
+
+ /**
+ * Returns the time in millis since a job was executed for this app, in elapsed realtime
+ * timebase. This value can be larger than the current elapsed realtime if the job was executed
+ * before the device was rebooted. The default value is {@link Long#MAX_VALUE}.
+ * @param packageName the app you're asking about.
+ * @param userId the user associated with the job.
+ * @return the time in millis since a job was last executed for the app, provided it was
+ * indicated here before by a call to {@link #setLastJobRunTime(String, int, long)}.
+ */
+ public abstract long getTimeSinceLastJobRun(String packageName, @UserIdInt int userId);
}
diff --git a/android/appwidget/AppWidgetManager.java b/android/appwidget/AppWidgetManager.java
index 37bb6b0..a55bbda 100644
--- a/android/appwidget/AppWidgetManager.java
+++ b/android/appwidget/AppWidgetManager.java
@@ -677,6 +677,34 @@
}
/**
+ * Updates the info for the supplied AppWidget provider.
+ *
+ * <p>
+ * The manifest entry of the provider should contain an additional meta-data tag similar to
+ * {@link #META_DATA_APPWIDGET_PROVIDER} which should point to any additional definitions for
+ * the provider.
+ *
+ * <p>
+ * This is persisted across device reboots and app updates. If this meta-data key is not
+ * present in the manifest entry, the info reverts to default.
+ *
+ * @param provider {@link ComponentName} for the {@link
+ * android.content.BroadcastReceiver BroadcastReceiver} provider for your AppWidget.
+ * @param metaDataKey key for the meta-data tag pointing to the new provider info. Use null
+ * to reset any previously set info.
+ */
+ public void updateAppWidgetProviderInfo(ComponentName provider, @Nullable String metaDataKey) {
+ if (mService == null) {
+ return;
+ }
+ try {
+ mService.updateAppWidgetProviderInfo(provider, metaDataKey);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Notifies the specified collection view in all the specified AppWidget instances
* to invalidate their data.
*
diff --git a/android/arch/lifecycle/ComputableLiveData.java b/android/arch/lifecycle/ComputableLiveData.java
index 1ddcb1a..f135244 100644
--- a/android/arch/lifecycle/ComputableLiveData.java
+++ b/android/arch/lifecycle/ComputableLiveData.java
@@ -1,136 +1,9 @@
-/*
- * Copyright (C) 2017 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.
- */
-
+//ComputableLiveData interface for tests
package android.arch.lifecycle;
-
-import android.arch.core.executor.ArchTaskExecutor;
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.VisibleForTesting;
-import android.support.annotation.WorkerThread;
-
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * A LiveData class that can be invalidated & computed on demand.
- * <p>
- * This is an internal class for now, might be public if we see the necessity.
- *
- * @param <T> The type of the live data
- * @hide internal
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+import android.arch.lifecycle.LiveData;
public abstract class ComputableLiveData<T> {
-
- private final LiveData<T> mLiveData;
-
- private AtomicBoolean mInvalid = new AtomicBoolean(true);
- private AtomicBoolean mComputing = new AtomicBoolean(false);
-
- /**
- * Creates a computable live data which is computed when there are active observers.
- * <p>
- * It can also be invalidated via {@link #invalidate()} which will result in a call to
- * {@link #compute()} if there are active observers (or when they start observing)
- */
- @SuppressWarnings("WeakerAccess")
- public ComputableLiveData() {
- mLiveData = new LiveData<T>() {
- @Override
- protected void onActive() {
- // TODO if we make this class public, we should accept an executor
- ArchTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable);
- }
- };
- }
-
- /**
- * Returns the LiveData managed by this class.
- *
- * @return A LiveData that is controlled by ComputableLiveData.
- */
- @SuppressWarnings("WeakerAccess")
- @NonNull
- public LiveData<T> getLiveData() {
- return mLiveData;
- }
-
- @VisibleForTesting
- final Runnable mRefreshRunnable = new Runnable() {
- @WorkerThread
- @Override
- public void run() {
- boolean computed;
- do {
- computed = false;
- // compute can happen only in 1 thread but no reason to lock others.
- if (mComputing.compareAndSet(false, true)) {
- // as long as it is invalid, keep computing.
- try {
- T value = null;
- while (mInvalid.compareAndSet(true, false)) {
- computed = true;
- value = compute();
- }
- if (computed) {
- mLiveData.postValue(value);
- }
- } finally {
- // release compute lock
- mComputing.set(false);
- }
- }
- // check invalid after releasing compute lock to avoid the following scenario.
- // Thread A runs compute()
- // Thread A checks invalid, it is false
- // Main thread sets invalid to true
- // Thread B runs, fails to acquire compute lock and skips
- // Thread A releases compute lock
- // We've left invalid in set state. The check below recovers.
- } while (computed && mInvalid.get());
- }
- };
-
- // invalidation check always happens on the main thread
- @VisibleForTesting
- final Runnable mInvalidationRunnable = new Runnable() {
- @MainThread
- @Override
- public void run() {
- boolean isActive = mLiveData.hasActiveObservers();
- if (mInvalid.compareAndSet(false, true)) {
- if (isActive) {
- // TODO if we make this class public, we should accept an executor.
- ArchTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable);
- }
- }
- }
- };
-
- /**
- * Invalidates the LiveData.
- * <p>
- * When there are active observers, this will trigger a call to {@link #compute()}.
- */
- public void invalidate() {
- ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
- }
-
- @SuppressWarnings("WeakerAccess")
- @WorkerThread
- protected abstract T compute();
+ public ComputableLiveData(){}
+ abstract protected T compute();
+ public LiveData<T> getLiveData() {return null;}
+ public void invalidate() {}
}
diff --git a/android/arch/lifecycle/GenericLifecycleObserver.java b/android/arch/lifecycle/GenericLifecycleObserver.java
index 59f09c4..4601478 100644
--- a/android/arch/lifecycle/GenericLifecycleObserver.java
+++ b/android/arch/lifecycle/GenericLifecycleObserver.java
@@ -16,10 +16,13 @@
package android.arch.lifecycle;
+import android.support.annotation.RestrictTo;
+
/**
* Internal class that can receive any lifecycle change and dispatch it to the receiver.
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressWarnings({"WeakerAccess", "unused"})
public interface GenericLifecycleObserver extends LifecycleObserver {
/**
diff --git a/android/arch/lifecycle/HolderFragment.java b/android/arch/lifecycle/HolderFragment.java
index 100d10a..ca5e181 100644
--- a/android/arch/lifecycle/HolderFragment.java
+++ b/android/arch/lifecycle/HolderFragment.java
@@ -19,6 +19,7 @@
import android.app.Activity;
import android.app.Application.ActivityLifecycleCallbacks;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.v4.app.Fragment;
@@ -34,7 +35,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class HolderFragment extends Fragment {
+public class HolderFragment extends Fragment implements ViewModelStoreOwner {
private static final String LOG_TAG = "ViewModelStores";
private static final HolderFragmentManager sHolderFragmentManager = new HolderFragmentManager();
@@ -69,6 +70,8 @@
mViewModelStore.clear();
}
+ @NonNull
+ @Override
public ViewModelStore getViewModelStore() {
return mViewModelStore;
}
diff --git a/android/arch/lifecycle/LifecycleFragment.java b/android/arch/lifecycle/LifecycleFragment.java
deleted file mode 100644
index c0da66b..0000000
--- a/android/arch/lifecycle/LifecycleFragment.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2017 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.arch.lifecycle;
-
-import android.support.v4.app.Fragment;
-
-/**
- * @deprecated Use {@link Fragment} instead of it.
- */
-@Deprecated
-public class LifecycleFragment extends Fragment {
-}
diff --git a/android/arch/lifecycle/LiveData.java b/android/arch/lifecycle/LiveData.java
index 5b09c32..3aea6ac 100644
--- a/android/arch/lifecycle/LiveData.java
+++ b/android/arch/lifecycle/LiveData.java
@@ -1,410 +1,4 @@
-/*
- * Copyright (C) 2017 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.
- */
-
+//LiveData interface for tests
package android.arch.lifecycle;
-
-import static android.arch.lifecycle.Lifecycle.State.DESTROYED;
-import static android.arch.lifecycle.Lifecycle.State.STARTED;
-
-import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.core.internal.SafeIterableMap;
-import android.arch.lifecycle.Lifecycle.State;
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import java.util.Iterator;
-import java.util.Map;
-
-/**
- * LiveData is a data holder class that can be observed within a given lifecycle.
- * This means that an {@link Observer} can be added in a pair with a {@link LifecycleOwner}, and
- * this observer will be notified about modifications of the wrapped data only if the paired
- * LifecycleOwner is in active state. LifecycleOwner is considered as active, if its state is
- * {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED}. An observer added via
- * {@link #observeForever(Observer)} is considered as always active and thus will be always notified
- * about modifications. For those observers, you should manually call
- * {@link #removeObserver(Observer)}.
- *
- * <p> An observer added with a Lifecycle will be automatically removed if the corresponding
- * Lifecycle moves to {@link Lifecycle.State#DESTROYED} state. This is especially useful for
- * activities and fragments where they can safely observe LiveData and not worry about leaks:
- * they will be instantly unsubscribed when they are destroyed.
- *
- * <p>
- * In addition, LiveData has {@link LiveData#onActive()} and {@link LiveData#onInactive()} methods
- * to get notified when number of active {@link Observer}s change between 0 and 1.
- * This allows LiveData to release any heavy resources when it does not have any Observers that
- * are actively observing.
- * <p>
- * This class is designed to hold individual data fields of {@link ViewModel},
- * but can also be used for sharing data between different modules in your application
- * in a decoupled fashion.
- *
- * @param <T> The type of data held by this instance
- * @see ViewModel
- */
-@SuppressWarnings({"WeakerAccess", "unused"})
-// TODO: Thread checks are too strict right now, we may consider automatically moving them to main
-// thread.
-public abstract class LiveData<T> {
- private final Object mDataLock = new Object();
- static final int START_VERSION = -1;
- private static final Object NOT_SET = new Object();
-
- private static final LifecycleOwner ALWAYS_ON = new LifecycleOwner() {
-
- private LifecycleRegistry mRegistry = init();
-
- private LifecycleRegistry init() {
- LifecycleRegistry registry = new LifecycleRegistry(this);
- registry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
- registry.handleLifecycleEvent(Lifecycle.Event.ON_START);
- registry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
- return registry;
- }
-
- @Override
- public Lifecycle getLifecycle() {
- return mRegistry;
- }
- };
-
- private SafeIterableMap<Observer<T>, LifecycleBoundObserver> mObservers =
- new SafeIterableMap<>();
-
- // how many observers are in active state
- private int mActiveCount = 0;
- private volatile Object mData = NOT_SET;
- // when setData is called, we set the pending data and actual data swap happens on the main
- // thread
- private volatile Object mPendingData = NOT_SET;
- private int mVersion = START_VERSION;
-
- private boolean mDispatchingValue;
- @SuppressWarnings("FieldCanBeLocal")
- private boolean mDispatchInvalidated;
- private final Runnable mPostValueRunnable = new Runnable() {
- @Override
- public void run() {
- Object newValue;
- synchronized (mDataLock) {
- newValue = mPendingData;
- mPendingData = NOT_SET;
- }
- //noinspection unchecked
- setValue((T) newValue);
- }
- };
-
- private void considerNotify(LifecycleBoundObserver observer) {
- if (!observer.active) {
- return;
- }
- // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
- //
- // we still first check observer.active to keep it as the entrance for events. So even if
- // the observer moved to an active state, if we've not received that event, we better not
- // notify for a more predictable notification order.
- if (!isActiveState(observer.owner.getLifecycle().getCurrentState())) {
- observer.activeStateChanged(false);
- return;
- }
- if (observer.lastVersion >= mVersion) {
- return;
- }
- observer.lastVersion = mVersion;
- //noinspection unchecked
- observer.observer.onChanged((T) mData);
- }
-
- private void dispatchingValue(@Nullable LifecycleBoundObserver initiator) {
- if (mDispatchingValue) {
- mDispatchInvalidated = true;
- return;
- }
- mDispatchingValue = true;
- do {
- mDispatchInvalidated = false;
- if (initiator != null) {
- considerNotify(initiator);
- initiator = null;
- } else {
- for (Iterator<Map.Entry<Observer<T>, LifecycleBoundObserver>> iterator =
- mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
- considerNotify(iterator.next().getValue());
- if (mDispatchInvalidated) {
- break;
- }
- }
- }
- } while (mDispatchInvalidated);
- mDispatchingValue = false;
- }
-
- /**
- * Adds the given observer to the observers list within the lifespan of the given
- * owner. The events are dispatched on the main thread. If LiveData already has data
- * set, it will be delivered to the observer.
- * <p>
- * The observer will only receive events if the owner is in {@link Lifecycle.State#STARTED}
- * or {@link Lifecycle.State#RESUMED} state (active).
- * <p>
- * If the owner moves to the {@link Lifecycle.State#DESTROYED} state, the observer will
- * automatically be removed.
- * <p>
- * When data changes while the {@code owner} is not active, it will not receive any updates.
- * If it becomes active again, it will receive the last available data automatically.
- * <p>
- * LiveData keeps a strong reference to the observer and the owner as long as the
- * given LifecycleOwner is not destroyed. When it is destroyed, LiveData removes references to
- * the observer & the owner.
- * <p>
- * If the given owner is already in {@link Lifecycle.State#DESTROYED} state, LiveData
- * ignores the call.
- * <p>
- * If the given owner, observer tuple is already in the list, the call is ignored.
- * If the observer is already in the list with another owner, LiveData throws an
- * {@link IllegalArgumentException}.
- *
- * @param owner The LifecycleOwner which controls the observer
- * @param observer The observer that will receive the events
- */
- @MainThread
- public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) {
- if (owner.getLifecycle().getCurrentState() == DESTROYED) {
- // ignore
- return;
- }
- LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
- LifecycleBoundObserver existing = mObservers.putIfAbsent(observer, wrapper);
- if (existing != null && existing.owner != wrapper.owner) {
- throw new IllegalArgumentException("Cannot add the same observer"
- + " with different lifecycles");
- }
- if (existing != null) {
- return;
- }
- owner.getLifecycle().addObserver(wrapper);
- }
-
- /**
- * Adds the given observer to the observers list. This call is similar to
- * {@link LiveData#observe(LifecycleOwner, Observer)} with a LifecycleOwner, which
- * is always active. This means that the given observer will receive all events and will never
- * be automatically removed. You should manually call {@link #removeObserver(Observer)} to stop
- * observing this LiveData.
- * While LiveData has one of such observers, it will be considered
- * as active.
- * <p>
- * If the observer was already added with an owner to this LiveData, LiveData throws an
- * {@link IllegalArgumentException}.
- *
- * @param observer The observer that will receive the events
- */
- @MainThread
- public void observeForever(@NonNull Observer<T> observer) {
- observe(ALWAYS_ON, observer);
- }
-
- /**
- * Removes the given observer from the observers list.
- *
- * @param observer The Observer to receive events.
- */
- @MainThread
- public void removeObserver(@NonNull final Observer<T> observer) {
- assertMainThread("removeObserver");
- LifecycleBoundObserver removed = mObservers.remove(observer);
- if (removed == null) {
- return;
- }
- removed.owner.getLifecycle().removeObserver(removed);
- removed.activeStateChanged(false);
- }
-
- /**
- * Removes all observers that are tied to the given {@link LifecycleOwner}.
- *
- * @param owner The {@code LifecycleOwner} scope for the observers to be removed.
- */
- @MainThread
- public void removeObservers(@NonNull final LifecycleOwner owner) {
- assertMainThread("removeObservers");
- for (Map.Entry<Observer<T>, LifecycleBoundObserver> entry : mObservers) {
- if (entry.getValue().owner == owner) {
- removeObserver(entry.getKey());
- }
- }
- }
-
- /**
- * Posts a task to a main thread to set the given value. So if you have a following code
- * executed in the main thread:
- * <pre class="prettyprint">
- * liveData.postValue("a");
- * liveData.setValue("b");
- * </pre>
- * The value "b" would be set at first and later the main thread would override it with
- * the value "a".
- * <p>
- * If you called this method multiple times before a main thread executed a posted task, only
- * the last value would be dispatched.
- *
- * @param value The new value
- */
- protected void postValue(T value) {
- boolean postTask;
- synchronized (mDataLock) {
- postTask = mPendingData == NOT_SET;
- mPendingData = value;
- }
- if (!postTask) {
- return;
- }
- ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
- }
-
- /**
- * Sets the value. If there are active observers, the value will be dispatched to them.
- * <p>
- * This method must be called from the main thread. If you need set a value from a background
- * thread, you can use {@link #postValue(Object)}
- *
- * @param value The new value
- */
- @MainThread
- protected void setValue(T value) {
- assertMainThread("setValue");
- mVersion++;
- mData = value;
- dispatchingValue(null);
- }
-
- /**
- * Returns the current value.
- * Note that calling this method on a background thread does not guarantee that the latest
- * value set will be received.
- *
- * @return the current value
- */
- @Nullable
- public T getValue() {
- Object data = mData;
- if (data != NOT_SET) {
- //noinspection unchecked
- return (T) data;
- }
- return null;
- }
-
- int getVersion() {
- return mVersion;
- }
-
- /**
- * Called when the number of active observers change to 1 from 0.
- * <p>
- * This callback can be used to know that this LiveData is being used thus should be kept
- * up to date.
- */
- protected void onActive() {
-
- }
-
- /**
- * Called when the number of active observers change from 1 to 0.
- * <p>
- * This does not mean that there are no observers left, there may still be observers but their
- * lifecycle states aren't {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED}
- * (like an Activity in the back stack).
- * <p>
- * You can check if there are observers via {@link #hasObservers()}.
- */
- protected void onInactive() {
-
- }
-
- /**
- * Returns true if this LiveData has observers.
- *
- * @return true if this LiveData has observers
- */
- public boolean hasObservers() {
- return mObservers.size() > 0;
- }
-
- /**
- * Returns true if this LiveData has active observers.
- *
- * @return true if this LiveData has active observers
- */
- public boolean hasActiveObservers() {
- return mActiveCount > 0;
- }
-
- class LifecycleBoundObserver implements GenericLifecycleObserver {
- public final LifecycleOwner owner;
- public final Observer<T> observer;
- public boolean active;
- public int lastVersion = START_VERSION;
-
- LifecycleBoundObserver(LifecycleOwner owner, Observer<T> observer) {
- this.owner = owner;
- this.observer = observer;
- }
-
- @Override
- public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
- if (owner.getLifecycle().getCurrentState() == DESTROYED) {
- removeObserver(observer);
- return;
- }
- // immediately set active state, so we'd never dispatch anything to inactive
- // owner
- activeStateChanged(isActiveState(owner.getLifecycle().getCurrentState()));
- }
-
- void activeStateChanged(boolean newActive) {
- if (newActive == active) {
- return;
- }
- active = newActive;
- boolean wasInactive = LiveData.this.mActiveCount == 0;
- LiveData.this.mActiveCount += active ? 1 : -1;
- if (wasInactive && active) {
- onActive();
- }
- if (LiveData.this.mActiveCount == 0 && !active) {
- onInactive();
- }
- if (active) {
- dispatchingValue(this);
- }
- }
- }
-
- static boolean isActiveState(State state) {
- return state.isAtLeast(STARTED);
- }
-
- private void assertMainThread(String methodName) {
- if (!ArchTaskExecutor.getInstance().isMainThread()) {
- throw new IllegalStateException("Cannot invoke " + methodName + " on a background"
- + " thread");
- }
- }
+public class LiveData<T> {
}
diff --git a/android/arch/lifecycle/LiveDataTest.java b/android/arch/lifecycle/LiveDataTest.java
index c1dc54d..046059b 100644
--- a/android/arch/lifecycle/LiveDataTest.java
+++ b/android/arch/lifecycle/LiveDataTest.java
@@ -30,6 +30,7 @@
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.only;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -37,11 +38,12 @@
import static org.mockito.Mockito.when;
import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.lifecycle.util.InstantTaskExecutor;
+import android.arch.core.executor.testing.InstantTaskExecutorRule;
import android.support.annotation.Nullable;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -52,6 +54,10 @@
@SuppressWarnings({"unchecked"})
@RunWith(JUnit4.class)
public class LiveDataTest {
+
+ @Rule
+ public InstantTaskExecutorRule mInstantTaskExecutorRule = new InstantTaskExecutorRule();
+
private PublicLiveData<String> mLiveData;
private MethodExec mActiveObserversChanged;
@@ -102,11 +108,6 @@
when(mOwner4.getLifecycle()).thenReturn(mLifecycle4);
}
- @Before
- public void swapExecutorDelegate() {
- ArchTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor());
- }
-
@After
public void removeExecutorDelegate() {
ArchTaskExecutor.getInstance().setDelegate(null);
@@ -779,6 +780,28 @@
verify(mObserver4, never()).onChanged(anyString());
}
+ @Test
+ public void nestedForeverObserver() {
+ mLiveData.setValue(".");
+ mLiveData.observeForever(new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ mLiveData.observeForever(mock(Observer.class));
+ mLiveData.removeObserver(this);
+ }
+ });
+ verify(mActiveObserversChanged, only()).onCall(true);
+ }
+
+ @Test
+ public void readdForeverObserver() {
+ Observer observer = mock(Observer.class);
+ mLiveData.observeForever(observer);
+ mLiveData.observeForever(observer);
+ mLiveData.removeObserver(observer);
+ assertThat(mLiveData.hasObservers(), is(false));
+ }
+
private GenericLifecycleObserver getGenericLifecycleObserver(Lifecycle lifecycle) {
ArgumentCaptor<GenericLifecycleObserver> captor =
ArgumentCaptor.forClass(GenericLifecycleObserver.class);
diff --git a/android/arch/lifecycle/ThreadedLiveDataTest.java b/android/arch/lifecycle/ThreadedLiveDataTest.java
index ca27067..3366641 100644
--- a/android/arch/lifecycle/ThreadedLiveDataTest.java
+++ b/android/arch/lifecycle/ThreadedLiveDataTest.java
@@ -30,10 +30,13 @@
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+@RunWith(JUnit4.class)
public class ThreadedLiveDataTest {
private static final int TIMEOUT_SECS = 3;
diff --git a/android/arch/lifecycle/TransformationsTest.java b/android/arch/lifecycle/TransformationsTest.java
index 940a3e8..02397da 100644
--- a/android/arch/lifecycle/TransformationsTest.java
+++ b/android/arch/lifecycle/TransformationsTest.java
@@ -21,6 +21,7 @@
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.only;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -191,4 +192,25 @@
verify(observer, never()).onChanged(anyString());
assertThat(first.hasObservers(), is(false));
}
+
+ @Test
+ public void noObsoleteValueTest() {
+ MutableLiveData<Integer> numbers = new MutableLiveData<>();
+ LiveData<Integer> squared = Transformations.map(numbers, new Function<Integer, Integer>() {
+ @Override
+ public Integer apply(Integer input) {
+ return input * input;
+ }
+ });
+
+ Observer observer = mock(Observer.class);
+ squared.setValue(1);
+ squared.observeForever(observer);
+ verify(observer).onChanged(1);
+ squared.removeObserver(observer);
+ reset(observer);
+ numbers.setValue(2);
+ squared.observeForever(observer);
+ verify(observer, only()).onChanged(4);
+ }
}
diff --git a/android/arch/lifecycle/ViewModelProvider.java b/android/arch/lifecycle/ViewModelProvider.java
index a7b3aeb..e01aa19 100644
--- a/android/arch/lifecycle/ViewModelProvider.java
+++ b/android/arch/lifecycle/ViewModelProvider.java
@@ -16,9 +16,12 @@
package android.arch.lifecycle;
+import android.app.Application;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
+import java.lang.reflect.InvocationTargetException;
+
/**
* An utility class that provides {@code ViewModels} for a scope.
* <p>
@@ -152,4 +155,57 @@
}
}
}
+
+ /**
+ * {@link Factory} which may create {@link AndroidViewModel} and
+ * {@link ViewModel}, which have an empty constructor.
+ */
+ public static class AndroidViewModelFactory extends ViewModelProvider.NewInstanceFactory {
+
+ private static AndroidViewModelFactory sInstance;
+
+ /**
+ * Retrieve a singleton instance of AndroidViewModelFactory.
+ *
+ * @param application an application to pass in {@link AndroidViewModel}
+ * @return A valid {@link AndroidViewModelFactory}
+ */
+ public static AndroidViewModelFactory getInstance(@NonNull Application application) {
+ if (sInstance == null) {
+ sInstance = new AndroidViewModelFactory(application);
+ }
+ return sInstance;
+ }
+
+ private Application mApplication;
+
+ /**
+ * Creates a {@code AndroidViewModelFactory}
+ *
+ * @param application an application to pass in {@link AndroidViewModel}
+ */
+ public AndroidViewModelFactory(@NonNull Application application) {
+ mApplication = application;
+ }
+
+ @NonNull
+ @Override
+ public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+ if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
+ //noinspection TryWithIdenticalCatches
+ try {
+ return modelClass.getConstructor(Application.class).newInstance(mApplication);
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ } catch (InstantiationException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ }
+ }
+ return super.create(modelClass);
+ }
+ }
}
diff --git a/android/arch/lifecycle/ViewModelProviderTest.java b/android/arch/lifecycle/ViewModelProviderTest.java
index 37d2020..142f19a 100644
--- a/android/arch/lifecycle/ViewModelProviderTest.java
+++ b/android/arch/lifecycle/ViewModelProviderTest.java
@@ -21,6 +21,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import android.arch.lifecycle.ViewModelProvider.NewInstanceFactory;
+import android.support.annotation.NonNull;
import org.junit.Assert;
import org.junit.Before;
@@ -72,6 +73,7 @@
public void testOwnedBy() {
final ViewModelStore store = new ViewModelStore();
ViewModelStoreOwner owner = new ViewModelStoreOwner() {
+ @NonNull
@Override
public ViewModelStore getViewModelStore() {
return store;
diff --git a/android/arch/lifecycle/ViewModelProviders.java b/android/arch/lifecycle/ViewModelProviders.java
index b4b20aa..d9894a8 100644
--- a/android/arch/lifecycle/ViewModelProviders.java
+++ b/android/arch/lifecycle/ViewModelProviders.java
@@ -16,7 +16,6 @@
package android.arch.lifecycle;
-import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import android.arch.lifecycle.ViewModelProvider.Factory;
@@ -25,20 +24,16 @@
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
-import java.lang.reflect.InvocationTargetException;
-
/**
* Utilities methods for {@link ViewModelStore} class.
*/
public class ViewModelProviders {
- @SuppressLint("StaticFieldLeak")
- private static DefaultFactory sDefaultFactory;
-
- private static void initializeFactoryIfNeeded(Application application) {
- if (sDefaultFactory == null) {
- sDefaultFactory = new DefaultFactory(application);
- }
+ /**
+ * @deprecated This class should not be directly instantiated
+ */
+ @Deprecated
+ public ViewModelProviders() {
}
private static Application checkApplication(Activity activity) {
@@ -62,30 +57,36 @@
* Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given
* {@code fragment} is alive. More detailed explanation is in {@link ViewModel}.
* <p>
- * It uses {@link DefaultFactory} to instantiate new ViewModels.
+ * It uses {@link ViewModelProvider.AndroidViewModelFactory} to instantiate new ViewModels.
*
* @param fragment a fragment, in whose scope ViewModels should be retained
* @return a ViewModelProvider instance
*/
+ @NonNull
@MainThread
public static ViewModelProvider of(@NonNull Fragment fragment) {
- initializeFactoryIfNeeded(checkApplication(checkActivity(fragment)));
- return new ViewModelProvider(ViewModelStores.of(fragment), sDefaultFactory);
+ ViewModelProvider.AndroidViewModelFactory factory =
+ ViewModelProvider.AndroidViewModelFactory.getInstance(
+ checkApplication(checkActivity(fragment)));
+ return new ViewModelProvider(ViewModelStores.of(fragment), factory);
}
/**
* Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given Activity
* is alive. More detailed explanation is in {@link ViewModel}.
* <p>
- * It uses {@link DefaultFactory} to instantiate new ViewModels.
+ * It uses {@link ViewModelProvider.AndroidViewModelFactory} to instantiate new ViewModels.
*
* @param activity an activity, in whose scope ViewModels should be retained
* @return a ViewModelProvider instance
*/
+ @NonNull
@MainThread
public static ViewModelProvider of(@NonNull FragmentActivity activity) {
- initializeFactoryIfNeeded(checkApplication(activity));
- return new ViewModelProvider(ViewModelStores.of(activity), sDefaultFactory);
+ ViewModelProvider.AndroidViewModelFactory factory =
+ ViewModelProvider.AndroidViewModelFactory.getInstance(
+ checkApplication(activity));
+ return new ViewModelProvider(ViewModelStores.of(activity), factory);
}
/**
@@ -98,6 +99,7 @@
* @param factory a {@code Factory} to instantiate new ViewModels
* @return a ViewModelProvider instance
*/
+ @NonNull
@MainThread
public static ViewModelProvider of(@NonNull Fragment fragment, @NonNull Factory factory) {
checkApplication(checkActivity(fragment));
@@ -114,6 +116,7 @@
* @param factory a {@code Factory} to instantiate new ViewModels
* @return a ViewModelProvider instance
*/
+ @NonNull
@MainThread
public static ViewModelProvider of(@NonNull FragmentActivity activity,
@NonNull Factory factory) {
@@ -124,39 +127,22 @@
/**
* {@link Factory} which may create {@link AndroidViewModel} and
* {@link ViewModel}, which have an empty constructor.
+ *
+ * @deprecated Use {@link ViewModelProvider.AndroidViewModelFactory}
*/
@SuppressWarnings("WeakerAccess")
- public static class DefaultFactory extends ViewModelProvider.NewInstanceFactory {
-
- private Application mApplication;
-
+ @Deprecated
+ public static class DefaultFactory extends ViewModelProvider.AndroidViewModelFactory {
/**
- * Creates a {@code DefaultFactory}
+ * Creates a {@code AndroidViewModelFactory}
*
* @param application an application to pass in {@link AndroidViewModel}
+ * @deprecated Use {@link ViewModelProvider.AndroidViewModelFactory} or
+ * {@link ViewModelProvider.AndroidViewModelFactory#getInstance(Application)}.
*/
+ @Deprecated
public DefaultFactory(@NonNull Application application) {
- mApplication = application;
- }
-
- @NonNull
- @Override
- public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
- if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
- //noinspection TryWithIdenticalCatches
- try {
- return modelClass.getConstructor(Application.class).newInstance(mApplication);
- } catch (NoSuchMethodException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- } catch (IllegalAccessException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- } catch (InstantiationException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- } catch (InvocationTargetException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- }
- }
- return super.create(modelClass);
+ super(application);
}
}
}
diff --git a/android/arch/lifecycle/ViewModelStores.java b/android/arch/lifecycle/ViewModelStores.java
index e79c934..348a06e 100644
--- a/android/arch/lifecycle/ViewModelStores.java
+++ b/android/arch/lifecycle/ViewModelStores.java
@@ -38,6 +38,7 @@
* @param activity an activity whose {@code ViewModelStore} is requested
* @return a {@code ViewModelStore}
*/
+ @NonNull
@MainThread
public static ViewModelStore of(@NonNull FragmentActivity activity) {
if (activity instanceof ViewModelStoreOwner) {
@@ -52,6 +53,7 @@
* @param fragment a fragment whose {@code ViewModelStore} is requested
* @return a {@code ViewModelStore}
*/
+ @NonNull
@MainThread
public static ViewModelStore of(@NonNull Fragment fragment) {
if (fragment instanceof ViewModelStoreOwner) {
diff --git a/android/arch/paging/ContiguousPagedList.java b/android/arch/paging/ContiguousPagedList.java
index 42eb320..c622f65 100644
--- a/android/arch/paging/ContiguousPagedList.java
+++ b/android/arch/paging/ContiguousPagedList.java
@@ -58,8 +58,11 @@
mStorage.appendPage(page, ContiguousPagedList.this);
} else if (resultType == PageResult.PREPEND) {
mStorage.prependPage(page, ContiguousPagedList.this);
+ } else {
+ throw new IllegalArgumentException("unexpected resultType " + resultType);
}
+
if (mBoundaryCallback != null) {
boolean deferEmpty = mStorage.size() == 0;
boolean deferBegin = !deferEmpty
diff --git a/android/arch/paging/DataSource.java b/android/arch/paging/DataSource.java
index bbf7ccb..9f51539 100644
--- a/android/arch/paging/DataSource.java
+++ b/android/arch/paging/DataSource.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 2018 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.
@@ -16,280 +16,7 @@
package android.arch.paging;
-import android.support.annotation.AnyThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.WorkerThread;
-
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Base class for loading pages of snapshot data into a {@link PagedList}.
- * <p>
- * DataSource is queried to load pages of content into a {@link PagedList}. A PagedList can grow as
- * it loads more data, but the data loaded cannot be updated. If the underlying data set is
- * modified, a new PagedList / DataSource pair must be created to represent the new data.
- * <h4>Loading Pages</h4>
- * PagedList queries data from its DataSource in response to loading hints. {@link PagedListAdapter}
- * calls {@link PagedList#loadAround(int)} to load content as the user scrolls in a RecyclerView.
- * <p>
- * To control how and when a PagedList queries data from its DataSource, see
- * {@link PagedList.Config}. The Config object defines things like load sizes and prefetch distance.
- * <h4>Updating Paged Data</h4>
- * A PagedList / DataSource pair are a snapshot of the data set. A new pair of
- * PagedList / DataSource must be created if an update occurs, such as a reorder, insert, delete, or
- * content update occurs. A DataSource must detect that it cannot continue loading its
- * snapshot (for instance, when Database query notices a table being invalidated), and call
- * {@link #invalidate()}. Then a new PagedList / DataSource pair would be created to load data from
- * the new state of the Database query.
- * <p>
- * To page in data that doesn't update, you can create a single DataSource, and pass it to a single
- * PagedList. For example, loading from network when the network's paging API doesn't provide
- * updates.
- * <p>
- * To page in data from a source that does provide updates, you can create a
- * {@link DataSource.Factory}, where each DataSource created is invalidated when an update to the
- * data set occurs that makes the current snapshot invalid. For example, when paging a query from
- * the Database, and the table being queried inserts or removes items. You can also use a
- * DataSource.Factory to provide multiple versions of network-paged lists. If reloading all content
- * (e.g. in response to an action like swipe-to-refresh) is required to get a new version of data,
- * you can connect an explicit refresh signal to call {@link #invalidate()} on the current
- * DataSource.
- * <p>
- * If you have more granular update signals, such as a network API signaling an update to a single
- * item in the list, it's recommended to load data from network into memory. Then present that
- * data to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory
- * copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the
- * snapshot can be created.
- * <h4>Implementing a DataSource</h4>
- * To implement, extend one of the subclasses: {@link PageKeyedDataSource},
- * {@link ItemKeyedDataSource}, or {@link PositionalDataSource}.
- * <p>
- * Use {@link PageKeyedDataSource} if pages you load embed keys for loading adjacent pages. For
- * example a network response that returns some items, and a next/previous page links.
- * <p>
- * Use {@link ItemKeyedDataSource} if you need to use data from item {@code N-1} to load item
- * {@code N}. For example, if requesting the backend for the next comments in the list
- * requires the ID or timestamp of the most recent loaded comment, or if querying the next users
- * from a name-sorted database query requires the name and unique ID of the previous.
- * <p>
- * Use {@link PositionalDataSource} if you can load pages of a requested size at arbitrary
- * positions, and provide a fixed item count. PositionalDataSource supports querying pages at
- * arbitrary positions, so can provide data to PagedLists in arbitrary order. Note that
- * PositionalDataSource is required to respect page size for efficient tiling. If you want to
- * override page size (e.g. when network page size constraints are only known at runtime), use one
- * of the other DataSource classes.
- * <p>
- * Because a {@code null} item indicates a placeholder in {@link PagedList}, DataSource may not
- * return {@code null} items in lists that it loads. This is so that users of the PagedList
- * can differentiate unloaded placeholder items from content that has been paged in.
- *
- * @param <Key> Input used to trigger initial load from the DataSource. Often an Integer position.
- * @param <Value> Value type loaded by the DataSource.
- */
-@SuppressWarnings("unused") // suppress warning to remove Key/Value, needed for subclass type safety
-public abstract class DataSource<Key, Value> {
- /**
- * Factory for DataSources.
- * <p>
- * Data-loading systems of an application or library can implement this interface to allow
- * {@code LiveData<PagedList>}s to be created. For example, Room can provide a
- * DataSource.Factory for a given SQL query:
- *
- * <pre>
- * {@literal @}Dao
- * interface UserDao {
- * {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
- * public abstract DataSource.Factory<Integer, User> usersByLastName();
- * }
- * </pre>
- * In the above sample, {@code Integer} is used because it is the {@code Key} type of
- * PositionalDataSource. Currently, Room uses the {@code LIMIT}/{@code OFFSET} SQL keywords to
- * page a large query with a PositionalDataSource.
- *
- * @param <Key> Key identifying items in DataSource.
- * @param <Value> Type of items in the list loaded by the DataSources.
- */
+abstract public class DataSource<K, T> {
public interface Factory<Key, Value> {
- /**
- * Create a DataSource.
- * <p>
- * The DataSource should invalidate itself if the snapshot is no longer valid. If a
- * DataSource becomes invalid, the only way to query more data is to create a new DataSource
- * from the Factory.
- * <p>
- * {@link LivePagedListBuilder} for example will construct a new PagedList and DataSource
- * when the current DataSource is invalidated, and pass the new PagedList through the
- * {@code LiveData<PagedList>} to observers.
- *
- * @return the new DataSource.
- */
- DataSource<Key, Value> create();
- }
-
- // Since we currently rely on implementation details of two implementations,
- // prevent external subclassing, except through exposed subclasses
- DataSource() {
- }
-
- /**
- * Returns true if the data source guaranteed to produce a contiguous set of items,
- * never producing gaps.
- */
- abstract boolean isContiguous();
-
- static class BaseLoadCallback<T> {
- static void validateInitialLoadParams(@NonNull List<?> data, int position, int totalCount) {
- if (position < 0) {
- throw new IllegalArgumentException("Position must be non-negative");
- }
- if (data.size() + position > totalCount) {
- throw new IllegalArgumentException(
- "List size + position too large, last item in list beyond totalCount.");
- }
- if (data.size() == 0 && totalCount > 0) {
- throw new IllegalArgumentException(
- "Initial result cannot be empty if items are present in data set.");
- }
- }
-
- @PageResult.ResultType
- final int mResultType;
- private final DataSource mDataSource;
- private final PageResult.Receiver<T> mReceiver;
-
- // mSignalLock protects mPostExecutor, and mHasSignalled
- private final Object mSignalLock = new Object();
- private Executor mPostExecutor = null;
- private boolean mHasSignalled = false;
-
- BaseLoadCallback(@NonNull DataSource dataSource, @PageResult.ResultType int resultType,
- @Nullable Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
- mDataSource = dataSource;
- mResultType = resultType;
- mPostExecutor = mainThreadExecutor;
- mReceiver = receiver;
- }
-
- void setPostExecutor(Executor postExecutor) {
- synchronized (mSignalLock) {
- mPostExecutor = postExecutor;
- }
- }
-
- /**
- * Call before verifying args, or dispatching actul results
- *
- * @return true if DataSource was invalid, and invalid result dispatched
- */
- boolean dispatchInvalidResultIfInvalid() {
- if (mDataSource.isInvalid()) {
- dispatchResultToReceiver(PageResult.<T>getInvalidResult());
- return true;
- }
- return false;
- }
-
- void dispatchResultToReceiver(final @NonNull PageResult<T> result) {
- Executor executor;
- synchronized (mSignalLock) {
- if (mHasSignalled) {
- throw new IllegalStateException(
- "callback.onResult already called, cannot call again.");
- }
- mHasSignalled = true;
- executor = mPostExecutor;
- }
-
- if (executor != null) {
- executor.execute(new Runnable() {
- @Override
- public void run() {
- mReceiver.onPageResult(mResultType, result);
- }
- });
- } else {
- mReceiver.onPageResult(mResultType, result);
- }
- }
- }
-
- /**
- * Invalidation callback for DataSource.
- * <p>
- * Used to signal when a DataSource a data source has become invalid, and that a new data source
- * is needed to continue loading data.
- */
- public interface InvalidatedCallback {
- /**
- * Called when the data backing the list has become invalid. This callback is typically used
- * to signal that a new data source is needed.
- * <p>
- * This callback will be invoked on the thread that calls {@link #invalidate()}. It is valid
- * for the data source to invalidate itself during its load methods, or for an outside
- * source to invalidate it.
- */
- @AnyThread
- void onInvalidated();
- }
-
- private AtomicBoolean mInvalid = new AtomicBoolean(false);
-
- private CopyOnWriteArrayList<InvalidatedCallback> mOnInvalidatedCallbacks =
- new CopyOnWriteArrayList<>();
-
- /**
- * Add a callback to invoke when the DataSource is first invalidated.
- * <p>
- * Once invalidated, a data source will not become valid again.
- * <p>
- * A data source will only invoke its callbacks once - the first time {@link #invalidate()}
- * is called, on that thread.
- *
- * @param onInvalidatedCallback The callback, will be invoked on thread that
- * {@link #invalidate()} is called on.
- */
- @AnyThread
- @SuppressWarnings("WeakerAccess")
- public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
- mOnInvalidatedCallbacks.add(onInvalidatedCallback);
- }
-
- /**
- * Remove a previously added invalidate callback.
- *
- * @param onInvalidatedCallback The previously added callback.
- */
- @AnyThread
- @SuppressWarnings("WeakerAccess")
- public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
- mOnInvalidatedCallbacks.remove(onInvalidatedCallback);
- }
-
- /**
- * Signal the data source to stop loading, and notify its callback.
- * <p>
- * If invalidate has already been called, this method does nothing.
- */
- @AnyThread
- public void invalidate() {
- if (mInvalid.compareAndSet(false, true)) {
- for (InvalidatedCallback callback : mOnInvalidatedCallbacks) {
- callback.onInvalidated();
- }
- }
- }
-
- /**
- * Returns true if the data source is invalid, and can no longer be queried for data.
- *
- * @return True if the data source is invalid, and can no longer return data.
- */
- @WorkerThread
- public boolean isInvalid() {
- return mInvalid.get();
}
}
diff --git a/android/arch/paging/LivePagedListProvider.java b/android/arch/paging/LivePagedListProvider.java
index 44b71a8..74334ee 100644
--- a/android/arch/paging/LivePagedListProvider.java
+++ b/android/arch/paging/LivePagedListProvider.java
@@ -22,8 +22,8 @@
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
-// NOTE: Room 1.0 depends on this class, so it should not be removed
-// until Room switches to using DataSource.Factory directly
+// NOTE: Room 1.0 depends on this class, so it should not be removed until
+// we can require a version of Room that uses DataSource.Factory directly
/**
* Provides a {@code LiveData<PagedList>}, given a means to construct a DataSource.
* <p>
diff --git a/android/arch/paging/PagedListAdapter.java b/android/arch/paging/PagedListAdapter.java
index a8158c2..be23271 100644
--- a/android/arch/paging/PagedListAdapter.java
+++ b/android/arch/paging/PagedListAdapter.java
@@ -50,7 +50,7 @@
* class MyViewModel extends ViewModel {
* public final LiveData<PagedList<User>> usersList;
* public MyViewModel(UserDao userDao) {
- * usersList = LivePagedListBuilder<>(
+ * usersList = new LivePagedListBuilder<>(
* userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
* }
* }
diff --git a/android/arch/paging/PagedListAdapterHelper.java b/android/arch/paging/PagedListAdapterHelper.java
index 7a0b81a..ba8ffab 100644
--- a/android/arch/paging/PagedListAdapterHelper.java
+++ b/android/arch/paging/PagedListAdapterHelper.java
@@ -54,7 +54,7 @@
* class MyViewModel extends ViewModel {
* public final LiveData<PagedList<User>> usersList;
* public MyViewModel(UserDao userDao) {
- * usersList = LivePagedListBuilder<>(
+ * usersList = new LivePagedListBuilder<>(
* userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
* }
* }
@@ -72,10 +72,8 @@
* }
*
* class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
- * private final PagedListAdapterHelper<User> mHelper;
- * public UserAdapter(PagedListAdapterHelper.Builder<User> builder) {
- * mHelper = new PagedListAdapterHelper(this, DIFF_CALLBACK);
- * }
+ * private final PagedListAdapterHelper<User> mHelper
+ * = new PagedListAdapterHelper(this, DIFF_CALLBACK);
* {@literal @}Override
* public int getItemCount() {
* return mHelper.getItemCount();
diff --git a/android/arch/paging/PositionalDataSource.java b/android/arch/paging/PositionalDataSource.java
index 780bcf6..7ffce00 100644
--- a/android/arch/paging/PositionalDataSource.java
+++ b/android/arch/paging/PositionalDataSource.java
@@ -172,7 +172,9 @@
if (position + data.size() != totalCount
&& data.size() % mPageSize != 0) {
throw new IllegalArgumentException("PositionalDataSource requires initial load"
- + " size to be a multiple of page size to support internal tiling.");
+ + " size to be a multiple of page size to support internal tiling."
+ + " loadSize " + data.size() + ", position " + position
+ + ", totalCount " + totalCount + ", pageSize " + mPageSize);
}
if (mCountingEnabled) {
@@ -236,9 +238,10 @@
*/
public static class LoadRangeCallback<T> extends BaseLoadCallback<T> {
private final int mPositionOffset;
- LoadRangeCallback(@NonNull PositionalDataSource dataSource, int positionOffset,
+ LoadRangeCallback(@NonNull PositionalDataSource dataSource,
+ @PageResult.ResultType int resultType, int positionOffset,
Executor mainThreadExecutor, PageResult.Receiver<T> receiver) {
- super(dataSource, PageResult.TILE, mainThreadExecutor, receiver);
+ super(dataSource, resultType, mainThreadExecutor, receiver);
mPositionOffset = positionOffset;
}
@@ -272,10 +275,11 @@
callback.setPostExecutor(mainThreadExecutor);
}
- final void dispatchLoadRange(int startPosition, int count,
- @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
- LoadRangeCallback<T> callback =
- new LoadRangeCallback<>(this, startPosition, mainThreadExecutor, receiver);
+ final void dispatchLoadRange(@PageResult.ResultType int resultType, int startPosition,
+ int count, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<T> receiver) {
+ LoadRangeCallback<T> callback = new LoadRangeCallback<>(
+ this, resultType, startPosition, mainThreadExecutor, receiver);
if (count == 0) {
callback.onResult(Collections.<T>emptyList());
} else {
@@ -467,7 +471,7 @@
@NonNull PageResult.Receiver<Value> receiver) {
int startIndex = currentEndIndex + 1;
mPositionalDataSource.dispatchLoadRange(
- startIndex, pageSize, mainThreadExecutor, receiver);
+ PageResult.APPEND, startIndex, pageSize, mainThreadExecutor, receiver);
}
@Override
@@ -479,12 +483,12 @@
if (startIndex < 0) {
// trigger empty list load
mPositionalDataSource.dispatchLoadRange(
- startIndex, 0, mainThreadExecutor, receiver);
+ PageResult.PREPEND, startIndex, 0, mainThreadExecutor, receiver);
} else {
int loadSize = Math.min(pageSize, startIndex + 1);
startIndex = startIndex - loadSize + 1;
mPositionalDataSource.dispatchLoadRange(
- startIndex, loadSize, mainThreadExecutor, receiver);
+ PageResult.PREPEND, startIndex, loadSize, mainThreadExecutor, receiver);
}
}
diff --git a/android/arch/paging/TiledDataSource.java b/android/arch/paging/TiledDataSource.java
index 77695e5..7285aa4 100644
--- a/android/arch/paging/TiledDataSource.java
+++ b/android/arch/paging/TiledDataSource.java
@@ -23,6 +23,8 @@
import java.util.Collections;
import java.util.List;
+// NOTE: Room 1.0 depends on this class, so it should not be removed until
+// we can require a version of Room that uses PositionalDataSource directly
/**
* @param <T> Type loaded by the TiledDataSource.
*
@@ -60,9 +62,11 @@
// convert from legacy behavior
List<T> list = loadRange(firstLoadPosition, firstLoadSize);
- if (list != null) {
+ if (list != null && list.size() == firstLoadSize) {
callback.onResult(list, firstLoadPosition, totalCount);
} else {
+ // null list, or size doesn't match request
+ // The size check is a WAR for Room 1.0, subsequent versions do the check in Room
invalidate();
}
}
diff --git a/android/arch/paging/TiledPagedList.java b/android/arch/paging/TiledPagedList.java
index f7aae98..9958b8d 100644
--- a/android/arch/paging/TiledPagedList.java
+++ b/android/arch/paging/TiledPagedList.java
@@ -44,6 +44,10 @@
return;
}
+ if (type != PageResult.INIT && type != PageResult.TILE) {
+ throw new IllegalArgumentException("unexpected resultType" + type);
+ }
+
if (mStorage.getPageCount() == 0) {
mStorage.initAndSplit(
pageResult.leadingNulls, pageResult.page, pageResult.trailingNulls,
@@ -179,7 +183,7 @@
int startPosition = pageIndex * pageSize;
int count = Math.min(pageSize, mStorage.size() - startPosition);
mDataSource.dispatchLoadRange(
- startPosition, count, mMainThreadExecutor, mReceiver);
+ PageResult.TILE, startPosition, count, mMainThreadExecutor, mReceiver);
}
}
});
diff --git a/android/arch/persistence/db/SimpleSQLiteQuery.java b/android/arch/persistence/db/SimpleSQLiteQuery.java
index e2a3829..bcf4f49 100644
--- a/android/arch/persistence/db/SimpleSQLiteQuery.java
+++ b/android/arch/persistence/db/SimpleSQLiteQuery.java
@@ -17,8 +17,8 @@
package android.arch.persistence.db;
/**
- * A basic implemtation of {@link SupportSQLiteQuery} which receives a query and its args and binds
- * args based on the passed in Object type.
+ * A basic implementation of {@link SupportSQLiteQuery} which receives a query and its args and
+ * binds args based on the passed in Object type.
*/
public final class SimpleSQLiteQuery implements SupportSQLiteQuery {
private final String mQuery;
diff --git a/android/arch/persistence/room/BuilderTest.java b/android/arch/persistence/room/BuilderTest.java
index 0728cca..2c9b9e7 100644
--- a/android/arch/persistence/room/BuilderTest.java
+++ b/android/arch/persistence/room/BuilderTest.java
@@ -30,6 +30,7 @@
import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory;
import android.arch.persistence.room.migration.Migration;
import android.content.Context;
+import android.support.annotation.NonNull;
import org.hamcrest.CoreMatchers;
import org.junit.Test;
@@ -108,15 +109,119 @@
}
@Test
+ public void migrationDowngrade() {
+ Migration m1_2 = new EmptyMigration(1, 2);
+ Migration m2_3 = new EmptyMigration(2, 3);
+ Migration m3_4 = new EmptyMigration(3, 4);
+ Migration m3_2 = new EmptyMigration(3, 2);
+ Migration m2_1 = new EmptyMigration(2, 1);
+ TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
+ .addMigrations(m1_2, m2_3, m3_4, m3_2, m2_1).build();
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ RoomDatabase.MigrationContainer migrations = config.migrationContainer;
+ assertThat(migrations.findMigrationPath(3, 2), is(asList(m3_2)));
+ assertThat(migrations.findMigrationPath(3, 1), is(asList(m3_2, m2_1)));
+ }
+
+ @Test
public void skipMigration() {
Context context = mock(Context.class);
+
TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
- .fallbackToDestructiveMigration().build();
+ .fallbackToDestructiveMigration()
+ .build();
+
DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
assertThat(config.requireMigration, is(false));
}
@Test
+ public void fallbackToDestructiveMigrationFrom_calledOnce_migrationsNotRequiredForValues() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigrationFrom(1, 2).build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(1), is(false));
+ assertThat(config.isMigrationRequiredFrom(2), is(false));
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_calledTwice_migrationsNotRequiredForValues() {
+ Context context = mock(Context.class);
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigrationFrom(1, 2)
+ .fallbackToDestructiveMigrationFrom(3, 4)
+ .build();
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+
+ assertThat(config.isMigrationRequiredFrom(1), is(false));
+ assertThat(config.isMigrationRequiredFrom(2), is(false));
+ assertThat(config.isMigrationRequiredFrom(3), is(false));
+ assertThat(config.isMigrationRequiredFrom(4), is(false));
+ }
+
+ @Test
+ public void isMigrationRequiredFrom_fallBackToDestructiveCalled_alwaysReturnsFalse() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigration()
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(0), is(false));
+ assertThat(config.isMigrationRequiredFrom(1), is(false));
+ assertThat(config.isMigrationRequiredFrom(5), is(false));
+ assertThat(config.isMigrationRequiredFrom(12), is(false));
+ assertThat(config.isMigrationRequiredFrom(132), is(false));
+ }
+
+ @Test
+ public void isMigrationRequiredFrom_byDefault_alwaysReturnsTrue() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(0), is(true));
+ assertThat(config.isMigrationRequiredFrom(1), is(true));
+ assertThat(config.isMigrationRequiredFrom(5), is(true));
+ assertThat(config.isMigrationRequiredFrom(12), is(true));
+ assertThat(config.isMigrationRequiredFrom(132), is(true));
+ }
+
+ @Test
+ public void isMigrationRequiredFrom_fallBackToDestFromCalled_falseForProvidedValues() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigrationFrom(1, 4, 81)
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(1), is(false));
+ assertThat(config.isMigrationRequiredFrom(4), is(false));
+ assertThat(config.isMigrationRequiredFrom(81), is(false));
+ }
+
+ @Test
+ public void isMigrationRequiredFrom_fallBackToDestFromCalled_trueForNonProvidedValues() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigrationFrom(1, 4, 81)
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(2), is(true));
+ assertThat(config.isMigrationRequiredFrom(3), is(true));
+ assertThat(config.isMigrationRequiredFrom(73), is(true));
+ }
+
+ @Test
public void createBasic() {
Context context = mock(Context.class);
TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
@@ -163,7 +268,7 @@
}
@Override
- public void migrate(SupportSQLiteDatabase database) {
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
}
}
diff --git a/android/arch/persistence/room/ColumnInfo.java b/android/arch/persistence/room/ColumnInfo.java
index 65da379..32b5818 100644
--- a/android/arch/persistence/room/ColumnInfo.java
+++ b/android/arch/persistence/room/ColumnInfo.java
@@ -68,7 +68,7 @@
* collation sequence to the column, and SQLite treats it like {@link #BINARY}.
*
* @return The collation sequence of the column. This is either {@link #UNSPECIFIED},
- * {@link #BINARY}, {@link #NOCASE}, or {@link #RTRIM}.
+ * {@link #BINARY}, {@link #NOCASE}, {@link #RTRIM}, {@link #LOCALIZED} or {@link #UNICODE}.
*/
@Collate int collate() default UNSPECIFIED;
@@ -141,8 +141,20 @@
* @see #collate()
*/
int RTRIM = 4;
+ /**
+ * Collation sequence that uses system's current locale.
+ *
+ * @see #collate()
+ */
+ int LOCALIZED = 5;
+ /**
+ * Collation sequence that uses Unicode Collation Algorithm.
+ *
+ * @see #collate()
+ */
+ int UNICODE = 6;
- @IntDef({UNSPECIFIED, BINARY, NOCASE, RTRIM})
+ @IntDef({UNSPECIFIED, BINARY, NOCASE, RTRIM, LOCALIZED, UNICODE})
@interface Collate {
}
}
diff --git a/android/arch/persistence/room/DatabaseConfiguration.java b/android/arch/persistence/room/DatabaseConfiguration.java
index adf5d4d..42acc1d 100644
--- a/android/arch/persistence/room/DatabaseConfiguration.java
+++ b/android/arch/persistence/room/DatabaseConfiguration.java
@@ -23,6 +23,7 @@
import android.support.annotation.RestrictTo;
import java.util.List;
+import java.util.Set;
/**
* Configuration class for a {@link RoomDatabase}.
@@ -65,6 +66,11 @@
public final boolean requireMigration;
/**
+ * The collection of schema versions from which migrations aren't required.
+ */
+ private final Set<Integer> mMigrationNotRequiredFrom;
+
+ /**
* Creates a database configuration with the given values.
*
* @param context The application context.
@@ -75,6 +81,8 @@
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param requireMigration True if Room should require a valid migration if version changes,
* instead of recreating the tables.
+ * @param migrationNotRequiredFrom The collection of schema versions from which migrations
+ * aren't required.
*
* @hide
*/
@@ -84,7 +92,8 @@
@NonNull RoomDatabase.MigrationContainer migrationContainer,
@Nullable List<RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
- boolean requireMigration) {
+ boolean requireMigration,
+ @Nullable Set<Integer> migrationNotRequiredFrom) {
this.sqliteOpenHelperFactory = sqliteOpenHelperFactory;
this.context = context;
this.name = name;
@@ -92,5 +101,21 @@
this.callbacks = callbacks;
this.allowMainThreadQueries = allowMainThreadQueries;
this.requireMigration = requireMigration;
+ this.mMigrationNotRequiredFrom = migrationNotRequiredFrom;
+ }
+
+ /**
+ * Returns whether a migration is required from the specified version.
+ *
+ * @param version The schema version.
+ * @return True if a valid migration is required, false otherwise.
+ */
+ public boolean isMigrationRequiredFrom(int version) {
+ // Migrations are required from this version if we generally require migrations AND EITHER
+ // there are no exceptions OR the supplied version is not one of the exceptions.
+ return requireMigration
+ && (mMigrationNotRequiredFrom == null
+ || !mMigrationNotRequiredFrom.contains(version));
+
}
}
diff --git a/android/arch/persistence/room/RawQuery.java b/android/arch/persistence/room/RawQuery.java
new file mode 100644
index 0000000..b41feab
--- /dev/null
+++ b/android/arch/persistence/room/RawQuery.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2018 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.arch.persistence.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a method in a {@link Dao} annotated class as a raw query method where you can pass the
+ * query as a {@link String} or a
+ * {@link android.arch.persistence.db.SupportSQLiteQuery SupportSQLiteQuery}.
+ * <pre>
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery
+ * User getUser(String query);
+ * {@literal @}RawQuery
+ * User getUserViaQuery(SupportSQLiteQuery query);
+ * }
+ * User user = rawDao.getUser("SELECT * FROM User WHERE id = 3 LIMIT 1");
+ * SimpleSQLiteQuery query = new SimpleSQLiteQuery("SELECT * FROM User WHERE id = ? LIMIT 1",
+ * new Object[]{3});
+ * User user2 = rawDao.getUserViaQuery(query);
+ * </pre>
+ * <p>
+ * Room will generate the code based on the return type of the function and failure to
+ * pass a proper query will result in a runtime failure or an undefined result.
+ * <p>
+ * If you know the query at compile time, you should always prefer {@link Query} since it validates
+ * the query at compile time and also generates more efficient code since Room can compute the
+ * query result at compile time (e.g. it does not need to account for possibly missing columns in
+ * the response).
+ * <p>
+ * On the other hand, {@code RawQuery} serves as an escape hatch where you can build your own
+ * SQL query at runtime but still use Room to convert it into objects.
+ * <p>
+ * {@code RawQuery} methods must return a non-void type. If you want to execute a raw query that
+ * does not return any value, use {@link android.arch.persistence.room.RoomDatabase#query
+ * RoomDatabase#query} methods.
+ * <p>
+ * <b>Observable Queries:</b>
+ * <p>
+ * {@code RawQuery} methods can return observable types but you need to specify which tables are
+ * accessed in the query using the {@link #observedEntities()} field in the annotation.
+ * <pre>
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery(observedEntities = User.class)
+ * LiveData<List<User>> getUsers(String query);
+ * }
+ * LiveData<List<User>> liveUsers = rawDao.getUsers("SELECT * FROM User ORDER BY name DESC");
+ * </pre>
+ * <b>Returning Pojos:</b>
+ * <p>
+ * RawQueries can also return plain old java objects, similar to {@link Query} methods.
+ * <pre>
+ * public class NameAndLastName {
+ * public final String name;
+ * public final String lastName;
+ *
+ * public NameAndLastName(String name, String lastName) {
+ * this.name = name;
+ * this.lastName = lastName;
+ * }
+ * }
+ *
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery
+ * NameAndLastName getNameAndLastName(String query);
+ * }
+ * NameAndLastName result = rawDao.getNameAndLastName("SELECT * FROM User WHERE id = 3")
+ * // or
+ * NameAndLastName result = rawDao.getNameAndLastName("SELECT name, lastName FROM User WHERE id =
+ * 3")
+ * </pre>
+ * <p>
+ * <b>Pojos with Embedded Fields:</b>
+ * <p>
+ * {@code RawQuery} methods can return pojos that include {@link Embedded} fields as well.
+ * <pre>
+ * public class UserAndPet {
+ * {@literal @}Embedded
+ * public User user;
+ * {@literal @}Embedded
+ * public Pet pet;
+ * }
+ *
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery
+ * UserAndPet getUserAndPet(String query);
+ * }
+ * UserAndPet received = rawDao.getUserAndPet(
+ * "SELECT * FROM User, Pet WHERE User.id = Pet.userId LIMIT 1")
+ * </pre>
+ *
+ * <b>Relations:</b>
+ * <p>
+ * {@code RawQuery} return types can also be objects with {@link Relation Relations}.
+ * <pre>
+ * public class UserAndAllPets {
+ * {@literal @}Embedded
+ * public User user;
+ * {@literal @}Relation(parentColumn = "id", entityColumn = "userId")
+ * public List<Pet> pets;
+ * }
+ *
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery
+ * List<UserAndAllPets> getUsersAndAllPets(String query);
+ * }
+ * List<UserAndAllPets> result = rawDao.getUsersAndAllPets("SELECT * FROM users");
+ * </pre>
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.CLASS)
+public @interface RawQuery {
+ /**
+ * Denotes the list of entities which are accessed in the provided query and should be observed
+ * for invalidation if the query is observable.
+ * <p>
+ * The listed classes should be {@link Entity Entities} that are linked from the containing
+ * {@link Database}.
+ * <p>
+ * Providing this field in a non-observable query has no impact.
+ * <pre>
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery(observedEntities = User.class)
+ * LiveData<List<User>> getUsers(String query);
+ * }
+ * LiveData<List<User>> liveUsers = rawDao.getUsers("select * from User ORDER BY name
+ * DESC");
+ * </pre>
+ *
+ * @return List of entities that should invalidate the query if changed.
+ */
+ Class[] observedEntities() default {};
+}
diff --git a/android/arch/persistence/room/RoomDatabase.java b/android/arch/persistence/room/RoomDatabase.java
index 70d832b..db7af1d 100644
--- a/android/arch/persistence/room/RoomDatabase.java
+++ b/android/arch/persistence/room/RoomDatabase.java
@@ -35,7 +35,9 @@
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@@ -333,7 +335,14 @@
/**
* Migrations, mapped by from-to pairs.
*/
- private MigrationContainer mMigrationContainer;
+ private final MigrationContainer mMigrationContainer;
+ private Set<Integer> mMigrationsNotRequiredFrom;
+ /**
+ * Keeps track of {@link Migration#startVersion}s and {@link Migration#endVersion}s added in
+ * {@link #addMigrations(Migration...)} for later validation that makes those versions don't
+ * match any versions passed to {@link #fallbackToDestructiveMigrationFrom(Integer...)}.
+ */
+ private Set<Integer> mMigrationStartAndEndVersions;
Builder(@NonNull Context context, @NonNull Class<T> klass, @Nullable String name) {
mContext = context;
@@ -376,7 +385,15 @@
* @return this
*/
@NonNull
- public Builder<T> addMigrations(@NonNull Migration... migrations) {
+ public Builder<T> addMigrations(@NonNull Migration... migrations) {
+ if (mMigrationStartAndEndVersions == null) {
+ mMigrationStartAndEndVersions = new HashSet<>();
+ }
+ for (Migration migration: migrations) {
+ mMigrationStartAndEndVersions.add(migration.startVersion);
+ mMigrationStartAndEndVersions.add(migration.endVersion);
+ }
+
mMigrationContainer.addMigrations(migrations);
return this;
}
@@ -423,6 +440,36 @@
}
/**
+ * Informs Room that it is allowed to destructively recreate database tables from specific
+ * starting schema versions.
+ * <p>
+ * This functionality is the same as that provided by
+ * {@link #fallbackToDestructiveMigration()}, except that this method allows the
+ * specification of a set of schema versions for which destructive recreation is allowed.
+ * <p>
+ * Using this method is preferable to {@link #fallbackToDestructiveMigration()} if you want
+ * to allow destructive migrations from some schema versions while still taking advantage
+ * of exceptions being thrown due to unintentionally missing migrations.
+ * <p>
+ * Note: No versions passed to this method may also exist as either starting or ending
+ * versions in the {@link Migration}s provided to {@link #addMigrations(Migration...)}. If a
+ * version passed to this method is found as a starting or ending version in a Migration, an
+ * exception will be thrown.
+ *
+ * @param startVersions The set of schema versions from which Room should use a destructive
+ * migration.
+ * @return this
+ */
+ @NonNull
+ public Builder<T> fallbackToDestructiveMigrationFrom(Integer... startVersions) {
+ if (mMigrationsNotRequiredFrom == null) {
+ mMigrationsNotRequiredFrom = new HashSet<>();
+ }
+ Collections.addAll(mMigrationsNotRequiredFrom, startVersions);
+ return this;
+ }
+
+ /**
* Adds a {@link Callback} to this database.
*
* @param callback The callback.
@@ -456,12 +503,28 @@
throw new IllegalArgumentException("Must provide an abstract class that"
+ " extends RoomDatabase");
}
+
+ if (mMigrationStartAndEndVersions != null && mMigrationsNotRequiredFrom != null) {
+ for (Integer version : mMigrationStartAndEndVersions) {
+ if (mMigrationsNotRequiredFrom.contains(version)) {
+ throw new IllegalArgumentException(
+ "Inconsistency detected. A Migration was supplied to "
+ + "addMigration(Migration... migrations) that has a start "
+ + "or end version equal to a start version supplied to "
+ + "fallbackToDestructiveMigrationFrom(Integer ... "
+ + "startVersions). Start version: "
+ + version);
+ }
+ }
+ }
+
if (mFactory == null) {
mFactory = new FrameworkSQLiteOpenHelperFactory();
}
DatabaseConfiguration configuration =
new DatabaseConfiguration(mContext, mName, mFactory, mMigrationContainer,
- mCallbacks, mAllowMainThreadQueries, mRequireMigration);
+ mCallbacks, mAllowMainThreadQueries, mRequireMigration,
+ mMigrationsNotRequiredFrom);
T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
db.init(configuration);
return db;
@@ -545,8 +608,14 @@
}
boolean found = false;
for (int i = firstIndex; i != lastIndex; i += searchDirection) {
- int targetVersion = targetNodes.keyAt(i);
- if (targetVersion <= end && targetVersion > start) {
+ final int targetVersion = targetNodes.keyAt(i);
+ final boolean shouldAddToPath;
+ if (upgrade) {
+ shouldAddToPath = targetVersion <= end && targetVersion > start;
+ } else {
+ shouldAddToPath = targetVersion >= end && targetVersion < start;
+ }
+ if (shouldAddToPath) {
result.add(targetNodes.valueAt(i));
start = targetVersion;
found = true;
diff --git a/android/arch/persistence/room/RoomOpenHelper.java b/android/arch/persistence/room/RoomOpenHelper.java
index 47279d6..aad6895 100644
--- a/android/arch/persistence/room/RoomOpenHelper.java
+++ b/android/arch/persistence/room/RoomOpenHelper.java
@@ -41,13 +41,20 @@
private final Delegate mDelegate;
@NonNull
private final String mIdentityHash;
+ /**
+ * Room v1 had a bug where the hash was not consistent if fields are reordered.
+ * The new has fixes it but we still need to accept the legacy hash.
+ */
+ @NonNull // b/64290754
+ private final String mLegacyHash;
public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
- @NonNull String identityHash) {
+ @NonNull String identityHash, @NonNull String legacyHash) {
super(delegate.version);
mConfiguration = configuration;
mDelegate = delegate;
mIdentityHash = identityHash;
+ mLegacyHash = legacyHash;
}
@Override
@@ -78,14 +85,17 @@
}
}
if (!migrated) {
- if (mConfiguration == null || mConfiguration.requireMigration) {
+ if (mConfiguration != null && !mConfiguration.isMigrationRequiredFrom(oldVersion)) {
+ mDelegate.dropAllTables(db);
+ mDelegate.createAllTables(db);
+ } else {
throw new IllegalStateException("A migration from " + oldVersion + " to "
- + newVersion + " is necessary. Please provide a Migration in the builder or call"
- + " fallbackToDestructiveMigration in the builder in which case Room will"
- + " re-create all of the tables.");
+ + newVersion + " was required but not found. Please provide the "
+ + "necessary Migration path via "
+ + "RoomDatabase.Builder.addMigration(Migration ...) or allow for "
+ + "destructive migrations via one of the "
+ + "RoomDatabase.Builder.fallbackToDestructiveMigration* methods.");
}
- mDelegate.dropAllTables(db);
- mDelegate.createAllTables(db);
}
}
@@ -115,7 +125,7 @@
} finally {
cursor.close();
}
- if (!mIdentityHash.equals(identityHash)) {
+ if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) {
throw new IllegalStateException("Room cannot verify the data integrity. Looks like"
+ " you've changed schema but forgot to update the version number. You can"
+ " simply fix this by increasing the version number.");
diff --git a/android/arch/persistence/room/integration/testapp/TestDatabase.java b/android/arch/persistence/room/integration/testapp/TestDatabase.java
index 610afb2..98282ab 100644
--- a/android/arch/persistence/room/integration/testapp/TestDatabase.java
+++ b/android/arch/persistence/room/integration/testapp/TestDatabase.java
@@ -25,6 +25,7 @@
import android.arch.persistence.room.integration.testapp.dao.PetCoupleDao;
import android.arch.persistence.room.integration.testapp.dao.PetDao;
import android.arch.persistence.room.integration.testapp.dao.ProductDao;
+import android.arch.persistence.room.integration.testapp.dao.RawDao;
import android.arch.persistence.room.integration.testapp.dao.SchoolDao;
import android.arch.persistence.room.integration.testapp.dao.SpecificDogDao;
import android.arch.persistence.room.integration.testapp.dao.ToyDao;
@@ -61,6 +62,7 @@
public abstract SpecificDogDao getSpecificDogDao();
public abstract WithClauseDao getWithClauseDao();
public abstract FunnyNamedDao getFunnyNamedDao();
+ public abstract RawDao getRawDao();
@SuppressWarnings("unused")
public static class Converters {
diff --git a/android/arch/persistence/room/integration/testapp/dao/PetDao.java b/android/arch/persistence/room/integration/testapp/dao/PetDao.java
index 5d060f4..e3a45a0 100644
--- a/android/arch/persistence/room/integration/testapp/dao/PetDao.java
+++ b/android/arch/persistence/room/integration/testapp/dao/PetDao.java
@@ -17,9 +17,11 @@
package android.arch.persistence.room.integration.testapp.dao;
import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.Delete;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
+import android.arch.persistence.room.Transaction;
import android.arch.persistence.room.integration.testapp.vo.Pet;
import android.arch.persistence.room.integration.testapp.vo.PetWithToyIds;
@@ -38,4 +40,19 @@
@Query("SELECT * FROM Pet ORDER BY Pet.mPetId ASC")
List<PetWithToyIds> allPetsWithToyIds();
+
+ @Delete
+ void delete(Pet pet);
+
+ @Query("SELECT mPetId FROM Pet")
+ int[] allIds();
+
+ @Transaction
+ default void deleteAndInsert(Pet oldPet, Pet newPet, boolean shouldFail) {
+ delete(oldPet);
+ if (shouldFail) {
+ throw new RuntimeException();
+ }
+ insertOrReplace(newPet);
+ }
}
diff --git a/android/arch/persistence/room/integration/testapp/dao/RawDao.java b/android/arch/persistence/room/integration/testapp/dao/RawDao.java
new file mode 100644
index 0000000..b4469c0
--- /dev/null
+++ b/android/arch/persistence/room/integration/testapp/dao/RawDao.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018 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.arch.persistence.room.integration.testapp.dao;
+
+import android.arch.lifecycle.LiveData;
+import android.arch.persistence.db.SupportSQLiteQuery;
+import android.arch.persistence.room.ColumnInfo;
+import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.RawQuery;
+import android.arch.persistence.room.integration.testapp.vo.NameAndLastName;
+import android.arch.persistence.room.integration.testapp.vo.User;
+import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets;
+import android.arch.persistence.room.integration.testapp.vo.UserAndPet;
+
+import java.util.Date;
+import java.util.List;
+
+@Dao
+public interface RawDao {
+ @RawQuery
+ User getUser(String query);
+ @RawQuery
+ UserAndAllPets getUserAndAllPets(String query);
+ @RawQuery
+ User getUser(SupportSQLiteQuery query);
+ @RawQuery
+ UserAndPet getUserAndPet(String query);
+ @RawQuery
+ NameAndLastName getUserNameAndLastName(String query);
+ @RawQuery(observedEntities = User.class)
+ NameAndLastName getUserNameAndLastName(SupportSQLiteQuery query);
+ @RawQuery
+ int count(String query);
+ @RawQuery
+ List<User> getUserList(String query);
+ @RawQuery
+ List<UserAndPet> getUserAndPetList(String query);
+ @RawQuery(observedEntities = User.class)
+ LiveData<User> getUserLiveData(String query);
+ @RawQuery
+ UserNameAndBirthday getUserAndBirthday(String query);
+ class UserNameAndBirthday {
+ @ColumnInfo(name = "mName")
+ public final String name;
+ @ColumnInfo(name = "mBirthday")
+ public final Date birthday;
+
+ public UserNameAndBirthday(String name, Date birthday) {
+ this.name = name;
+ this.birthday = birthday;
+ }
+ }
+}
diff --git a/android/arch/persistence/room/integration/testapp/dao/UserDao.java b/android/arch/persistence/room/integration/testapp/dao/UserDao.java
index 1a2a468..7cb8b60 100644
--- a/android/arch/persistence/room/integration/testapp/dao/UserDao.java
+++ b/android/arch/persistence/room/integration/testapp/dao/UserDao.java
@@ -18,8 +18,6 @@
import android.arch.lifecycle.LiveData;
import android.arch.paging.DataSource;
-import android.arch.paging.LivePagedListProvider;
-import android.arch.paging.TiledDataSource;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Delete;
import android.arch.persistence.room.Insert;
@@ -166,6 +164,12 @@
@Query("SELECT COUNT(*) from user")
public abstract int count();
+ @Query("SELECT mAdmin from User where mId = :uid")
+ public abstract boolean isAdmin(int uid);
+
+ @Query("SELECT mAdmin from User where mId = :uid")
+ public abstract LiveData<Boolean> isAdminLiveData(int uid);
+
public void insertBothByRunnable(final User a, final User b) {
mDatabase.runInTransaction(new Runnable() {
@Override
@@ -190,16 +194,10 @@
@Query("SELECT * FROM user where mAge > :age")
public abstract DataSource.Factory<Integer, User> loadPagedByAge(int age);
- @Query("SELECT * FROM user where mAge > :age")
- public abstract LivePagedListProvider<Integer, User> loadPagedByAge_legacy(int age);
-
// TODO: switch to PositionalDataSource once Room supports it
@Query("SELECT * FROM user ORDER BY mAge DESC")
public abstract DataSource.Factory<Integer, User> loadUsersByAgeDesc();
- @Query("SELECT * FROM user ORDER BY mAge DESC")
- public abstract TiledDataSource<User> loadUsersByAgeDesc_legacy();
-
@Query("DELETE FROM User WHERE mId IN (:ids) AND mAge == :age")
public abstract int deleteByAgeAndIds(int age, List<Integer> ids);
diff --git a/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java b/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
index 7fe2bc9..c850a4d 100644
--- a/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
+++ b/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
@@ -17,9 +17,13 @@
package android.arch.persistence.room.integration.testapp.migration;
import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.endsWith;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import android.arch.persistence.db.SupportSQLiteDatabase;
@@ -33,6 +37,7 @@
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import org.hamcrest.MatcherAssert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -252,6 +257,95 @@
db.close();
}
+ @Test
+ public void failWithIdentityCheck() throws IOException {
+ for (int i = 1; i < MigrationDb.LATEST_VERSION; i++) {
+ String name = "test_" + i;
+ helper.createDatabase(name, i).close();
+ IllegalStateException exception = null;
+ try {
+ MigrationDb db = Room.databaseBuilder(
+ InstrumentationRegistry.getInstrumentation().getTargetContext(),
+ MigrationDb.class, name).build();
+ db.runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ // do nothing
+ }
+ });
+ } catch (IllegalStateException ex) {
+ exception = ex;
+ }
+ MatcherAssert.assertThat("identity detection should've failed",
+ exception, notNullValue());
+ }
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_destructiveMigrationOccursForSuppliedVersion()
+ throws IOException {
+ SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 6);
+ final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(database);
+ dao.insertIntoEntity1(2, "foo");
+ dao.insertIntoEntity1(3, "bar");
+ database.close();
+ Context targetContext = InstrumentationRegistry.getTargetContext();
+
+ MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+ .fallbackToDestructiveMigrationFrom(6)
+ .build();
+
+ assertThat(db.dao().loadAllEntity1s().size(), is(0));
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_suppliedValueIsMigrationStartVersion_exception()
+ throws IOException {
+ SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 6);
+ database.close();
+ Context targetContext = InstrumentationRegistry.getTargetContext();
+
+ Throwable throwable = null;
+ try {
+ Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+ .addMigrations(MIGRATION_6_7)
+ .fallbackToDestructiveMigrationFrom(6)
+ .build();
+ } catch (Throwable t) {
+ throwable = t;
+ }
+
+ assertThat(throwable, is(not(nullValue())));
+ //noinspection ConstantConditions
+ assertThat(throwable.getMessage(),
+ startsWith("Inconsistency detected. A Migration was supplied to"));
+ assertThat(throwable.getMessage(), endsWith("6"));
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_suppliedValueIsMigrationEndVersion_exception()
+ throws IOException {
+ SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 5);
+ database.close();
+ Context targetContext = InstrumentationRegistry.getTargetContext();
+
+ Throwable throwable = null;
+ try {
+ Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+ .addMigrations(MIGRATION_5_6)
+ .fallbackToDestructiveMigrationFrom(6)
+ .build();
+ } catch (Throwable t) {
+ throwable = t;
+ }
+
+ assertThat(throwable, is(not(nullValue())));
+ //noinspection ConstantConditions
+ assertThat(throwable.getMessage(),
+ startsWith("Inconsistency detected. A Migration was supplied to"));
+ assertThat(throwable.getMessage(), endsWith("6"));
+ }
+
private void testFailure(int startVersion, int endVersion) throws IOException {
final SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, startVersion);
db.close();
diff --git a/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java b/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java
index b54abe8..0f68656 100644
--- a/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java
+++ b/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java
@@ -60,37 +60,13 @@
@Test
public void getUsersAsPagedList()
throws InterruptedException, ExecutionException, TimeoutException {
- validateUsersAsPagedList(new LivePagedListFactory() {
- @Override
- public LiveData<PagedList<User>> create() {
- return new LivePagedListBuilder<>(
- mUserDao.loadPagedByAge(3),
- new PagedList.Config.Builder()
- .setPageSize(10)
- .setPrefetchDistance(1)
- .setInitialLoadSizeHint(10).build())
- .build();
- }
- });
- }
-
-
- // TODO: delete this and factory abstraction when LivePagedListProvider is removed
- @Test
- public void getUsersAsPagedList_legacyLivePagedListProvider()
- throws InterruptedException, ExecutionException, TimeoutException {
- validateUsersAsPagedList(new LivePagedListFactory() {
- @Override
- public LiveData<PagedList<User>> create() {
- return mUserDao.loadPagedByAge_legacy(3).create(
- 0,
- new PagedList.Config.Builder()
- .setPageSize(10)
- .setPrefetchDistance(1)
- .setInitialLoadSizeHint(10)
- .build());
- }
- });
+ validateUsersAsPagedList(() -> new LivePagedListBuilder<>(
+ mUserDao.loadPagedByAge(3),
+ new PagedList.Config.Builder()
+ .setPageSize(10)
+ .setPrefetchDistance(1)
+ .setInitialLoadSizeHint(10).build())
+ .build());
}
private void validateUsersAsPagedList(LivePagedListFactory factory)
diff --git a/android/arch/persistence/room/integration/testapp/paging/LimitOffsetDataSourceTest.java b/android/arch/persistence/room/integration/testapp/paging/LimitOffsetDataSourceTest.java
index f0285a0..89359be 100644
--- a/android/arch/persistence/room/integration/testapp/paging/LimitOffsetDataSourceTest.java
+++ b/android/arch/persistence/room/integration/testapp/paging/LimitOffsetDataSourceTest.java
@@ -16,7 +16,7 @@
package android.arch.persistence.room.integration.testapp.paging;
-import static android.test.MoreAsserts.assertEmpty;
+import static junit.framework.Assert.assertFalse;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -45,15 +45,6 @@
mUserDao.deleteEverything();
}
- // TODO: delete this and factory abstraction when LivePagedListProvider is removed
- @Test
- public void limitOffsetDataSource_legacyTiledDataSource() {
- // Simple verification that loading a TiledDataSource still works.
- LimitOffsetDataSource<User> dataSource =
- (LimitOffsetDataSource<User>) mUserDao.loadUsersByAgeDesc_legacy();
- assertThat(dataSource.countItems(), is(0));
- }
-
private LimitOffsetDataSource<User> loadUsersByAgeDesc() {
return (LimitOffsetDataSource<User>) mUserDao.loadUsersByAgeDesc().create();
}
@@ -79,7 +70,7 @@
List<User> initial = dataSource.loadRange(0, 10);
assertThat(initial.get(0), is(users.get(0)));
- assertEmpty(dataSource.loadRange(1, 10));
+ assertFalse(dataSource.loadRange(1, 10).iterator().hasNext());
}
@Test
diff --git a/android/arch/persistence/room/integration/testapp/test/CollationTest.java b/android/arch/persistence/room/integration/testapp/test/CollationTest.java
new file mode 100644
index 0000000..7b0e933
--- /dev/null
+++ b/android/arch/persistence/room/integration/testapp/test/CollationTest.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2018 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.arch.persistence.room.integration.testapp.test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.arch.persistence.room.ColumnInfo;
+import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.Database;
+import android.arch.persistence.room.Insert;
+import android.arch.persistence.room.PrimaryKey;
+import android.arch.persistence.room.Query;
+import android.arch.persistence.room.Room;
+import android.arch.persistence.room.RoomDatabase;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CollationTest {
+ private CollateDb mDb;
+ private CollateDao mDao;
+ private Locale mDefaultLocale;
+ private final CollateEntity mItem1 = new CollateEntity(1, "abı");
+ private final CollateEntity mItem2 = new CollateEntity(2, "abi");
+ private final CollateEntity mItem3 = new CollateEntity(3, "abj");
+ private final CollateEntity mItem4 = new CollateEntity(4, "abç");
+
+ @Before
+ public void init() {
+ mDefaultLocale = Locale.getDefault();
+ }
+
+ private void initDao(Locale systemLocale) {
+ Locale.setDefault(systemLocale);
+ mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(),
+ CollateDb.class).build();
+ mDao = mDb.dao();
+ mDao.insert(mItem1);
+ mDao.insert(mItem2);
+ mDao.insert(mItem3);
+ mDao.insert(mItem4);
+ }
+
+ @After
+ public void closeDb() {
+ mDb.close();
+ Locale.setDefault(mDefaultLocale);
+ }
+
+ @Test
+ public void localized() {
+ initDao(new Locale("tr", "TR"));
+ List<CollateEntity> result = mDao.sortedByLocalized();
+ assertThat(result, CoreMatchers.is(Arrays.asList(
+ mItem4, mItem1, mItem2, mItem3
+ )));
+ }
+
+ @Test
+ public void localized_asUnicode() {
+ initDao(Locale.getDefault());
+ List<CollateEntity> result = mDao.sortedByLocalizedAsUnicode();
+ assertThat(result, CoreMatchers.is(Arrays.asList(
+ mItem4, mItem2, mItem1, mItem3
+ )));
+ }
+
+ @Test
+ public void unicode_asLocalized() {
+ initDao(new Locale("tr", "TR"));
+ List<CollateEntity> result = mDao.sortedByUnicodeAsLocalized();
+ assertThat(result, CoreMatchers.is(Arrays.asList(
+ mItem4, mItem1, mItem2, mItem3
+ )));
+ }
+
+ @Test
+ public void unicode() {
+ initDao(Locale.getDefault());
+ List<CollateEntity> result = mDao.sortedByUnicode();
+ assertThat(result, CoreMatchers.is(Arrays.asList(
+ mItem4, mItem2, mItem1, mItem3
+ )));
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @android.arch.persistence.room.Entity
+ static class CollateEntity {
+ @PrimaryKey
+ public final int id;
+ @ColumnInfo(collate = ColumnInfo.LOCALIZED)
+ public final String localizedName;
+ @ColumnInfo(collate = ColumnInfo.UNICODE)
+ public final String unicodeName;
+
+ CollateEntity(int id, String name) {
+ this.id = id;
+ this.localizedName = name;
+ this.unicodeName = name;
+ }
+
+ CollateEntity(int id, String localizedName, String unicodeName) {
+ this.id = id;
+ this.localizedName = localizedName;
+ this.unicodeName = unicodeName;
+ }
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ CollateEntity that = (CollateEntity) o;
+
+ if (id != that.id) return false;
+ if (!localizedName.equals(that.localizedName)) return false;
+ return unicodeName.equals(that.unicodeName);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id;
+ result = 31 * result + localizedName.hashCode();
+ result = 31 * result + unicodeName.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "CollateEntity{"
+ + "id=" + id
+ + ", localizedName='" + localizedName + '\''
+ + ", unicodeName='" + unicodeName + '\''
+ + '}';
+ }
+ }
+
+ @Dao
+ interface CollateDao {
+ @Query("SELECT * FROM CollateEntity ORDER BY localizedName ASC")
+ List<CollateEntity> sortedByLocalized();
+
+ @Query("SELECT * FROM CollateEntity ORDER BY localizedName COLLATE UNICODE ASC")
+ List<CollateEntity> sortedByLocalizedAsUnicode();
+
+ @Query("SELECT * FROM CollateEntity ORDER BY unicodeName ASC")
+ List<CollateEntity> sortedByUnicode();
+
+ @Query("SELECT * FROM CollateEntity ORDER BY unicodeName COLLATE LOCALIZED ASC")
+ List<CollateEntity> sortedByUnicodeAsLocalized();
+
+ @Insert
+ void insert(CollateEntity... entities);
+ }
+
+ @Database(entities = CollateEntity.class, version = 1, exportSchema = false)
+ abstract static class CollateDb extends RoomDatabase {
+ abstract CollateDao dao();
+ }
+}
diff --git a/android/arch/persistence/room/integration/testapp/test/DatabaseCallbackTest.java b/android/arch/persistence/room/integration/testapp/test/DatabaseCallbackTest.java
index 579b3e4..fafcc2c 100644
--- a/android/arch/persistence/room/integration/testapp/test/DatabaseCallbackTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/DatabaseCallbackTest.java
@@ -50,26 +50,38 @@
public void createAndOpen() {
Context context = InstrumentationRegistry.getTargetContext();
TestDatabaseCallback callback1 = new TestDatabaseCallback();
- TestDatabase db1 = Room.databaseBuilder(context, TestDatabase.class, "test")
- .addCallback(callback1)
- .build();
- assertFalse(callback1.mCreated);
- assertFalse(callback1.mOpened);
- User user1 = TestUtil.createUser(3);
- user1.setName("george");
- db1.getUserDao().insert(user1);
- assertTrue(callback1.mCreated);
- assertTrue(callback1.mOpened);
- TestDatabaseCallback callback2 = new TestDatabaseCallback();
- TestDatabase db2 = Room.databaseBuilder(context, TestDatabase.class, "test")
- .addCallback(callback2)
- .build();
- assertFalse(callback2.mCreated);
- assertFalse(callback2.mOpened);
- User user2 = db2.getUserDao().load(3);
- assertThat(user2.getName(), is("george"));
- assertFalse(callback2.mCreated); // Not called; already created by db1
- assertTrue(callback2.mOpened);
+ TestDatabase db1 = null;
+ TestDatabase db2 = null;
+ try {
+ db1 = Room.databaseBuilder(context, TestDatabase.class, "test")
+ .addCallback(callback1)
+ .build();
+ assertFalse(callback1.mCreated);
+ assertFalse(callback1.mOpened);
+ User user1 = TestUtil.createUser(3);
+ user1.setName("george");
+ db1.getUserDao().insert(user1);
+ assertTrue(callback1.mCreated);
+ assertTrue(callback1.mOpened);
+ TestDatabaseCallback callback2 = new TestDatabaseCallback();
+ db2 = Room.databaseBuilder(context, TestDatabase.class, "test")
+ .addCallback(callback2)
+ .build();
+ assertFalse(callback2.mCreated);
+ assertFalse(callback2.mOpened);
+ User user2 = db2.getUserDao().load(3);
+ assertThat(user2.getName(), is("george"));
+ assertFalse(callback2.mCreated); // Not called; already created by db1
+ assertTrue(callback2.mOpened);
+ } finally {
+ if (db1 != null) {
+ db1.close();
+ }
+ if (db2 != null) {
+ db2.close();
+ }
+ assertTrue(context.deleteDatabase("test"));
+ }
}
@Test
diff --git a/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java b/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java
index d78411f..d073598 100644
--- a/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java
@@ -315,6 +315,24 @@
assertThat(weakLiveData.get(), nullValue());
}
+ @Test
+ public void booleanLiveData() throws ExecutionException, InterruptedException,
+ TimeoutException {
+ User user = TestUtil.createUser(3);
+ user.setAdmin(false);
+ LiveData<Boolean> adminLiveData = mUserDao.isAdminLiveData(3);
+ final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
+ lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+ final TestObserver<Boolean> observer = new TestObserver<>();
+ observe(adminLiveData, lifecycleOwner, observer);
+ assertThat(observer.get(), is(nullValue()));
+ mUserDao.insert(user);
+ assertThat(observer.get(), is(false));
+ user.setAdmin(true);
+ mUserDao.insertOrReplace(user);
+ assertThat(observer.get(), is(true));
+ }
+
private void observe(final LiveData liveData, final LifecycleOwner provider,
final Observer observer) throws ExecutionException, InterruptedException {
FutureTask<Void> futureTask = new FutureTask<>(new Callable<Void>() {
diff --git a/android/arch/persistence/room/integration/testapp/test/PojoTest.java b/android/arch/persistence/room/integration/testapp/test/PojoTest.java
index b43e274..b1579fc 100644
--- a/android/arch/persistence/room/integration/testapp/test/PojoTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/PojoTest.java
@@ -19,15 +19,15 @@
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.runner.AndroidJUnit4;
-
import android.arch.persistence.room.Room;
import android.arch.persistence.room.integration.testapp.TestDatabase;
import android.arch.persistence.room.integration.testapp.dao.UserDao;
import android.arch.persistence.room.integration.testapp.vo.AvgWeightByAge;
import android.arch.persistence.room.integration.testapp.vo.User;
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
@@ -35,6 +35,7 @@
import java.util.Arrays;
+@LargeTest
@RunWith(AndroidJUnit4.class)
public class PojoTest {
private UserDao mUserDao;
diff --git a/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java b/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
index f076cf1..292e588 100644
--- a/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
@@ -28,7 +28,7 @@
import android.arch.paging.DataSource;
import android.arch.paging.LivePagedListBuilder;
import android.arch.paging.PagedList;
-import android.arch.paging.TiledDataSource;
+import android.arch.paging.PositionalDataSource;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Database;
import android.arch.persistence.room.Entity;
@@ -41,6 +41,7 @@
import android.arch.persistence.room.RoomDatabase;
import android.arch.persistence.room.RoomWarnings;
import android.arch.persistence.room.Transaction;
+import android.arch.persistence.room.paging.LimitOffsetDataSource;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.test.InstrumentationRegistry;
@@ -227,7 +228,8 @@
drain();
resetTransactionCount();
@SuppressWarnings("deprecation")
- TiledDataSource<Entity1> dataSource = mDao.dataSource();
+ LimitOffsetDataSource<Entity1> dataSource =
+ (LimitOffsetDataSource<Entity1>) mDao.dataSource();
dataSource.loadRange(0, 10);
assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 1 : 0));
}
@@ -371,7 +373,7 @@
DataSource.Factory<Integer, Entity1> pagedList();
- TiledDataSource<Entity1> dataSource();
+ PositionalDataSource<Entity1> dataSource();
@Insert
void insert(Entity1 entity1);
@@ -413,7 +415,7 @@
@Override
@Query(SELECT_ALL)
- TiledDataSource<Entity1> dataSource();
+ PositionalDataSource<Entity1> dataSource();
}
@Dao
@@ -456,7 +458,7 @@
@Override
@Transaction
@Query(SELECT_ALL)
- TiledDataSource<Entity1> dataSource();
+ PositionalDataSource<Entity1> dataSource();
}
@Database(version = 1, entities = {Entity1.class, Child.class}, exportSchema = false)
diff --git a/android/arch/persistence/room/integration/testapp/test/RawQueryTest.java b/android/arch/persistence/room/integration/testapp/test/RawQueryTest.java
new file mode 100644
index 0000000..4aae4ea
--- /dev/null
+++ b/android/arch/persistence/room/integration/testapp/test/RawQueryTest.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2018 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.arch.persistence.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.arch.core.executor.testing.CountingTaskExecutorRule;
+import android.arch.lifecycle.LiveData;
+import android.arch.persistence.db.SimpleSQLiteQuery;
+import android.arch.persistence.room.integration.testapp.dao.RawDao;
+import android.arch.persistence.room.integration.testapp.vo.NameAndLastName;
+import android.arch.persistence.room.integration.testapp.vo.Pet;
+import android.arch.persistence.room.integration.testapp.vo.User;
+import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets;
+import android.arch.persistence.room.integration.testapp.vo.UserAndPet;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RawQueryTest extends TestDatabaseTest {
+ @Rule
+ public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule();
+
+ @Test
+ public void entity_null() {
+ User user = mRawDao.getUser("SELECT * FROM User WHERE mId = 0");
+ assertThat(user, is(nullValue()));
+ }
+
+ @Test
+ public void entity_one() {
+ User expected = TestUtil.createUser(3);
+ mUserDao.insert(expected);
+ User received = mRawDao.getUser("SELECT * FROM User WHERE mId = 3");
+ assertThat(received, is(expected));
+ }
+
+ @Test
+ public void entity_list() {
+ List<User> expected = TestUtil.createUsersList(1, 2, 3, 4);
+ mUserDao.insertAll(expected.toArray(new User[4]));
+ List<User> received = mRawDao.getUserList("SELECT * FROM User ORDER BY mId ASC");
+ assertThat(received, is(expected));
+ }
+
+ @Test
+ public void entity_liveData() throws TimeoutException, InterruptedException {
+ LiveData<User> liveData = mRawDao.getUserLiveData("SELECT * FROM User WHERE mId = 3");
+ liveData.observeForever(user -> {
+ });
+ drain();
+ assertThat(liveData.getValue(), is(nullValue()));
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ drain();
+ assertThat(liveData.getValue(), is(user));
+ user.setLastName("cxZ");
+ mUserDao.insertOrReplace(user);
+ drain();
+ assertThat(liveData.getValue(), is(user));
+ }
+
+ @Test
+ public void entity_supportSql() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ SimpleSQLiteQuery query = new SimpleSQLiteQuery("SELECT * FROM User WHERE mId = ?",
+ new Object[]{3});
+ User received = mRawDao.getUser(query);
+ assertThat(received, is(user));
+ }
+
+ @Test
+ public void embedded() {
+ User user = TestUtil.createUser(3);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 1);
+ mUserDao.insert(user);
+ mPetDao.insertAll(pets);
+ UserAndPet received = mRawDao.getUserAndPet(
+ "SELECT * FROM User, Pet WHERE User.mId = Pet.mUserId LIMIT 1");
+ assertThat(received.getUser(), is(user));
+ assertThat(received.getPet(), is(pets[0]));
+ }
+
+ @Test
+ public void relation() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 10);
+ mPetDao.insertAll(pets);
+ UserAndAllPets result = mRawDao
+ .getUserAndAllPets("SELECT * FROM User WHERE mId = 3");
+ assertThat(result.user, is(user));
+ assertThat(result.pets, is(Arrays.asList(pets)));
+ }
+
+ @Test
+ public void pojo() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ NameAndLastName result =
+ mRawDao.getUserNameAndLastName("SELECT * FROM User");
+ assertThat(result, is(new NameAndLastName(user.getName(), user.getLastName())));
+ }
+
+ @Test
+ public void pojo_supportSql() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ NameAndLastName result =
+ mRawDao.getUserNameAndLastName(new SimpleSQLiteQuery(
+ "SELECT * FROM User WHERE mId = ?",
+ new Object[] {3}
+ ));
+ assertThat(result, is(new NameAndLastName(user.getName(), user.getLastName())));
+ }
+
+ @Test
+ public void pojo_typeConverter() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ RawDao.UserNameAndBirthday result = mRawDao.getUserAndBirthday(
+ "SELECT mName, mBirthday FROM user LIMIT 1");
+ assertThat(result.name, is(user.getName()));
+ assertThat(result.birthday, is(user.getBirthday()));
+ }
+
+ @Test
+ public void embedded_nullField() {
+ User user = TestUtil.createUser(3);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 1);
+ mUserDao.insert(user);
+ mPetDao.insertAll(pets);
+ UserAndPet received = mRawDao.getUserAndPet("SELECT * FROM User LIMIT 1");
+ assertThat(received.getUser(), is(user));
+ assertThat(received.getPet(), is(nullValue()));
+ }
+
+ @Test
+ public void embedded_list() {
+ User[] users = TestUtil.createUsersArray(3, 5);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 2);
+ mUserDao.insertAll(users);
+ mPetDao.insertAll(pets);
+ List<UserAndPet> received = mRawDao.getUserAndPetList(
+ "SELECT * FROM User LEFT JOIN Pet ON (User.mId = Pet.mUserId)"
+ + " ORDER BY mId ASC, mPetId ASC");
+ assertThat(received.size(), is(3));
+ // row 0
+ assertThat(received.get(0).getUser(), is(users[0]));
+ assertThat(received.get(0).getPet(), is(pets[0]));
+ // row 1
+ assertThat(received.get(1).getUser(), is(users[0]));
+ assertThat(received.get(1).getPet(), is(pets[1]));
+ // row 2
+ assertThat(received.get(2).getUser(), is(users[1]));
+ assertThat(received.get(2).getPet(), is(nullValue()));
+ }
+
+ @Test
+ public void count() {
+ mUserDao.insertAll(TestUtil.createUsersArray(3, 5, 7, 10));
+ int count = mRawDao.count("SELECT COUNT(*) FROM User");
+ assertThat(count, is(4));
+ }
+
+ private void drain() throws TimeoutException, InterruptedException {
+ mExecutorRule.drainTasks(1, TimeUnit.MINUTES);
+ }
+}
diff --git a/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java b/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
index de45ebb..793523c 100644
--- a/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
@@ -267,6 +267,18 @@
}
@Test
+ public void returnBoolean() {
+ User user1 = TestUtil.createUser(1);
+ User user2 = TestUtil.createUser(2);
+ user1.setAdmin(true);
+ user2.setAdmin(false);
+ mUserDao.insert(user1);
+ mUserDao.insert(user2);
+ assertThat(mUserDao.isAdmin(1), is(true));
+ assertThat(mUserDao.isAdmin(2), is(false));
+ }
+
+ @Test
public void findByCollateNoCase() {
User user = TestUtil.createUser(3);
user.setCustomField("abc");
@@ -483,6 +495,36 @@
}
@Test
+ public void transactionByDefaultImplementation() {
+ Pet pet1 = TestUtil.createPet(1);
+ mPetDao.insertOrReplace(pet1);
+ assertThat(mPetDao.count(), is(1));
+ assertThat(mPetDao.allIds()[0], is(1));
+ Pet pet2 = TestUtil.createPet(2);
+ mPetDao.deleteAndInsert(pet1, pet2, false);
+ assertThat(mPetDao.count(), is(1));
+ assertThat(mPetDao.allIds()[0], is(2));
+ }
+
+ @Test
+ public void transactionByDefaultImplementation_failure() {
+ Pet pet1 = TestUtil.createPet(1);
+ mPetDao.insertOrReplace(pet1);
+ assertThat(mPetDao.count(), is(1));
+ assertThat(mPetDao.allIds()[0], is(1));
+ Pet pet2 = TestUtil.createPet(2);
+ Throwable throwable = null;
+ try {
+ mPetDao.deleteAndInsert(pet1, pet2, true);
+ } catch (Throwable t) {
+ throwable = t;
+ }
+ assertNotNull("Was expecting an exception", throwable);
+ assertThat(mPetDao.count(), is(1));
+ assertThat(mPetDao.allIds()[0], is(1));
+ }
+
+ @Test
public void multipleInParamsFollowedByASingleParam_delete() {
User user = TestUtil.createUser(3);
user.setAge(30);
diff --git a/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java b/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java
index ec77561..e2525c4 100644
--- a/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java
@@ -21,6 +21,7 @@
import android.arch.persistence.room.integration.testapp.dao.FunnyNamedDao;
import android.arch.persistence.room.integration.testapp.dao.PetCoupleDao;
import android.arch.persistence.room.integration.testapp.dao.PetDao;
+import android.arch.persistence.room.integration.testapp.dao.RawDao;
import android.arch.persistence.room.integration.testapp.dao.SchoolDao;
import android.arch.persistence.room.integration.testapp.dao.SpecificDogDao;
import android.arch.persistence.room.integration.testapp.dao.ToyDao;
@@ -44,6 +45,7 @@
protected SpecificDogDao mSpecificDogDao;
protected WithClauseDao mWithClauseDao;
protected FunnyNamedDao mFunnyNamedDao;
+ protected RawDao mRawDao;
@Before
public void createDb() {
@@ -58,5 +60,6 @@
mSpecificDogDao = mDatabase.getSpecificDogDao();
mWithClauseDao = mDatabase.getWithClauseDao();
mFunnyNamedDao = mDatabase.getFunnyNamedDao();
+ mRawDao = mDatabase.getRawDao();
}
}
diff --git a/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java b/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java
index 29e2554..a6e8223 100644
--- a/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java
+++ b/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java
@@ -33,4 +33,23 @@
public String getLastName() {
return mLastName;
}
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ NameAndLastName that = (NameAndLastName) o;
+
+ if (mName != null ? !mName.equals(that.mName) : that.mName != null) return false;
+ return mLastName != null ? mLastName.equals(that.mLastName) : that.mLastName == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mName != null ? mName.hashCode() : 0;
+ result = 31 * result + (mLastName != null ? mLastName.hashCode() : 0);
+ return result;
+ }
}
diff --git a/android/arch/persistence/room/migration/TableInfoTest.java b/android/arch/persistence/room/migration/TableInfoTest.java
index d88c02f..0eb35f6 100644
--- a/android/arch/persistence/room/migration/TableInfoTest.java
+++ b/android/arch/persistence/room/migration/TableInfoTest.java
@@ -31,6 +31,7 @@
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import android.util.Pair;
import org.junit.After;
import org.junit.Test;
@@ -41,6 +42,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -209,6 +211,28 @@
));
}
+ @Test
+ public void compatColumnTypes() {
+ // see:https://www.sqlite.org/datatype3.html 3.1
+ List<Pair<String, String>> testCases = Arrays.asList(
+ new Pair<>("TINYINT", "integer"),
+ new Pair<>("VARCHAR", "text"),
+ new Pair<>("DOUBLE", "real"),
+ new Pair<>("BOOLEAN", "numeric"),
+ new Pair<>("FLOATING POINT", "integer")
+ );
+ for (Pair<String, String> testCase : testCases) {
+ mDb = createDatabase(
+ "CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + "name " + testCase.first + ")");
+ TableInfo info = TableInfo.read(mDb, "foo");
+ assertThat(info, is(new TableInfo("foo",
+ toMap(new TableInfo.Column("id", "INTEGER", false, 1),
+ new TableInfo.Column("name", testCase.second, false, 0)),
+ Collections.<TableInfo.ForeignKey>emptySet())));
+ }
+ }
+
private static Map<String, TableInfo.Column> toMap(TableInfo.Column... columns) {
Map<String, TableInfo.Column> result = new HashMap<>();
for (TableInfo.Column column : columns) {
diff --git a/android/arch/persistence/room/migration/bundle/DatabaseBundle.java b/android/arch/persistence/room/migration/bundle/DatabaseBundle.java
index 4ac9029..f131838 100644
--- a/android/arch/persistence/room/migration/bundle/DatabaseBundle.java
+++ b/android/arch/persistence/room/migration/bundle/DatabaseBundle.java
@@ -32,7 +32,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class DatabaseBundle {
+public class DatabaseBundle implements SchemaEquality<DatabaseBundle> {
@SerializedName("version")
private int mVersion;
@SerializedName("identityHash")
@@ -104,4 +104,10 @@
result.addAll(mSetupQueries);
return result;
}
+
+ @Override
+ public boolean isSchemaEqual(DatabaseBundle other) {
+ return SchemaEqualityUtil.checkSchemaEquality(getEntitiesByTableName(),
+ other.getEntitiesByTableName());
+ }
}
diff --git a/android/arch/persistence/room/migration/bundle/EntityBundle.java b/android/arch/persistence/room/migration/bundle/EntityBundle.java
index 8980a3b..d78ac35 100644
--- a/android/arch/persistence/room/migration/bundle/EntityBundle.java
+++ b/android/arch/persistence/room/migration/bundle/EntityBundle.java
@@ -16,6 +16,8 @@
package android.arch.persistence.room.migration.bundle;
+import static android.arch.persistence.room.migration.bundle.SchemaEqualityUtil.checkSchemaEquality;
+
import android.support.annotation.RestrictTo;
import com.google.gson.annotations.SerializedName;
@@ -35,7 +37,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class EntityBundle {
+public class EntityBundle implements SchemaEquality<EntityBundle> {
static final String NEW_TABLE_PREFIX = "_new_";
@@ -176,4 +178,15 @@
}
return result;
}
+
+ @Override
+ public boolean isSchemaEqual(EntityBundle other) {
+ if (!mTableName.equals(other.mTableName)) {
+ return false;
+ }
+ return checkSchemaEquality(getFieldsByColumnName(), other.getFieldsByColumnName())
+ && checkSchemaEquality(mPrimaryKey, other.mPrimaryKey)
+ && checkSchemaEquality(mIndices, other.mIndices)
+ && checkSchemaEquality(mForeignKeys, other.mForeignKeys);
+ }
}
diff --git a/android/arch/persistence/room/migration/bundle/EntityBundleTest.java b/android/arch/persistence/room/migration/bundle/EntityBundleTest.java
new file mode 100644
index 0000000..4b4df8b
--- /dev/null
+++ b/android/arch/persistence/room/migration/bundle/EntityBundleTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2017 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.arch.persistence.room.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import static java.util.Arrays.asList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Collections;
+
+@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+@RunWith(JUnit4.class)
+public class EntityBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo")),
+ asList(createForeignKeyBundle("bar", "foo")));
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo")),
+ asList(createForeignKeyBundle("bar", "foo")));
+
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_reorderedFields_equal() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("bar"), createFieldBundle("foo")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffFields_notEqual() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo2"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_reorderedForeignKeys_equal() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ asList(createForeignKeyBundle("x", "y"),
+ createForeignKeyBundle("bar", "foo")));
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ asList(createForeignKeyBundle("bar", "foo"),
+ createForeignKeyBundle("x", "y")));
+
+
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffForeignKeys_notEqual() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ asList(createForeignKeyBundle("bar", "foo")));
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ asList(createForeignKeyBundle("bar2", "foo")));
+
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_reorderedIndices_equal() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo"), createIndexBundle("baz")),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("baz"), createIndexBundle("foo")),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffIndices_notEqual() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo")),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo2")),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ private FieldBundle createFieldBundle(String name) {
+ return new FieldBundle("foo", name, "text", false);
+ }
+
+ private IndexBundle createIndexBundle(String colName) {
+ return new IndexBundle("ind_" + colName, false,
+ asList(colName), "create");
+ }
+
+ private ForeignKeyBundle createForeignKeyBundle(String targetTable, String column) {
+ return new ForeignKeyBundle(targetTable, "CASCADE", "CASCADE",
+ asList(column), asList(column));
+ }
+}
diff --git a/android/arch/persistence/room/migration/bundle/FieldBundle.java b/android/arch/persistence/room/migration/bundle/FieldBundle.java
index eb73d81..5f74087 100644
--- a/android/arch/persistence/room/migration/bundle/FieldBundle.java
+++ b/android/arch/persistence/room/migration/bundle/FieldBundle.java
@@ -27,7 +27,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class FieldBundle {
+public class FieldBundle implements SchemaEquality<FieldBundle> {
@SerializedName("fieldPath")
private String mFieldPath;
@SerializedName("columnName")
@@ -59,4 +59,14 @@
public boolean isNonNull() {
return mNonNull;
}
+
+ @Override
+ public boolean isSchemaEqual(FieldBundle other) {
+ if (mNonNull != other.mNonNull) return false;
+ if (mColumnName != null ? !mColumnName.equals(other.mColumnName)
+ : other.mColumnName != null) {
+ return false;
+ }
+ return mAffinity != null ? mAffinity.equals(other.mAffinity) : other.mAffinity == null;
+ }
}
diff --git a/android/arch/persistence/room/migration/bundle/FieldBundleTest.java b/android/arch/persistence/room/migration/bundle/FieldBundleTest.java
new file mode 100644
index 0000000..eac4477
--- /dev/null
+++ b/android/arch/persistence/room/migration/bundle/FieldBundleTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2017 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.arch.persistence.room.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FieldBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo", "foo", "text", false);
+ assertThat(bundle.isSchemaEqual(copy), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffNonNull_notEqual() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo", "foo", "text", true);
+ assertThat(bundle.isSchemaEqual(copy), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffColumnName_notEqual() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo", "foo2", "text", true);
+ assertThat(bundle.isSchemaEqual(copy), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffAffinity_notEqual() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo", "foo2", "int", false);
+ assertThat(bundle.isSchemaEqual(copy), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffPath_equal() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo>bar", "foo", "text", false);
+ assertThat(bundle.isSchemaEqual(copy), is(true));
+ }
+}
diff --git a/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java b/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java
index d72cf8c..367dd74 100644
--- a/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java
+++ b/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java
@@ -28,7 +28,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class ForeignKeyBundle {
+public class ForeignKeyBundle implements SchemaEquality<ForeignKeyBundle> {
@SerializedName("table")
private String mTable;
@SerializedName("onDelete")
@@ -43,10 +43,10 @@
/**
* Creates a foreign key bundle with the given parameters.
*
- * @param table The target table
- * @param onDelete OnDelete action
- * @param onUpdate OnUpdate action
- * @param columns The list of columns in the current table
+ * @param table The target table
+ * @param onDelete OnDelete action
+ * @param onUpdate OnUpdate action
+ * @param columns The list of columns in the current table
* @param referencedColumns The list of columns in the referenced table
*/
public ForeignKeyBundle(String table, String onDelete, String onUpdate,
@@ -102,4 +102,18 @@
public List<String> getReferencedColumns() {
return mReferencedColumns;
}
+
+ @Override
+ public boolean isSchemaEqual(ForeignKeyBundle other) {
+ if (mTable != null ? !mTable.equals(other.mTable) : other.mTable != null) return false;
+ if (mOnDelete != null ? !mOnDelete.equals(other.mOnDelete) : other.mOnDelete != null) {
+ return false;
+ }
+ if (mOnUpdate != null ? !mOnUpdate.equals(other.mOnUpdate) : other.mOnUpdate != null) {
+ return false;
+ }
+ // order matters
+ return mColumns.equals(other.mColumns) && mReferencedColumns.equals(
+ other.mReferencedColumns);
+ }
}
diff --git a/android/arch/persistence/room/migration/bundle/ForeignKeyBundleTest.java b/android/arch/persistence/room/migration/bundle/ForeignKeyBundleTest.java
new file mode 100644
index 0000000..be1b81e
--- /dev/null
+++ b/android/arch/persistence/room/migration/bundle/ForeignKeyBundleTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2017 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.arch.persistence.room.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class ForeignKeyBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffTable_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table2", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffOnDelete_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete2",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffOnUpdate_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate2", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffSrcOrder_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col2", "col1"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffTargetOrder_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target2", "target1"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+}
diff --git a/android/arch/persistence/room/migration/bundle/IndexBundle.java b/android/arch/persistence/room/migration/bundle/IndexBundle.java
index ba40618..e991316 100644
--- a/android/arch/persistence/room/migration/bundle/IndexBundle.java
+++ b/android/arch/persistence/room/migration/bundle/IndexBundle.java
@@ -28,7 +28,9 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class IndexBundle {
+public class IndexBundle implements SchemaEquality<IndexBundle> {
+ // should match Index.kt
+ public static final String DEFAULT_PREFIX = "index_";
@SerializedName("name")
private String mName;
@SerializedName("unique")
@@ -65,4 +67,25 @@
public String create(String tableName) {
return BundleUtil.replaceTableName(mCreateSql, tableName);
}
+
+ @Override
+ public boolean isSchemaEqual(IndexBundle other) {
+ if (mUnique != other.mUnique) return false;
+ if (mName.startsWith(DEFAULT_PREFIX)) {
+ if (!other.mName.startsWith(DEFAULT_PREFIX)) {
+ return false;
+ }
+ } else if (other.mName.startsWith(DEFAULT_PREFIX)) {
+ return false;
+ } else if (!mName.equals(other.mName)) {
+ return false;
+ }
+
+ // order matters
+ if (mColumnNames != null ? !mColumnNames.equals(other.mColumnNames)
+ : other.mColumnNames != null) {
+ return false;
+ }
+ return true;
+ }
}
diff --git a/android/arch/persistence/room/migration/bundle/IndexBundleTest.java b/android/arch/persistence/room/migration/bundle/IndexBundleTest.java
new file mode 100644
index 0000000..aa7230f
--- /dev/null
+++ b/android/arch/persistence/room/migration/bundle/IndexBundleTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017 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.arch.persistence.room.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class IndexBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffName_notEqual() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index3", false,
+ Arrays.asList("col1", "col2"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffGenericName_equal() {
+ IndexBundle bundle = new IndexBundle(IndexBundle.DEFAULT_PREFIX + "x", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle(IndexBundle.DEFAULT_PREFIX + "y", false,
+ Arrays.asList("col1", "col2"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffUnique_notEqual() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index1", true,
+ Arrays.asList("col1", "col2"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffColumns_notEqual() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index1", false,
+ Arrays.asList("col2", "col1"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffSql_equal() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql22");
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+}
diff --git a/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java b/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java
index c16f967..820aa7e 100644
--- a/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java
+++ b/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java
@@ -28,7 +28,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class PrimaryKeyBundle {
+public class PrimaryKeyBundle implements SchemaEquality<PrimaryKeyBundle> {
@SerializedName("columnNames")
private List<String> mColumnNames;
@SerializedName("autoGenerate")
@@ -46,4 +46,9 @@
public boolean isAutoGenerate() {
return mAutoGenerate;
}
+
+ @Override
+ public boolean isSchemaEqual(PrimaryKeyBundle other) {
+ return mColumnNames.equals(other.mColumnNames) && mAutoGenerate == other.mAutoGenerate;
+ }
}
diff --git a/android/arch/persistence/room/migration/bundle/PrimaryKeyBundleTest.java b/android/arch/persistence/room/migration/bundle/PrimaryKeyBundleTest.java
new file mode 100644
index 0000000..3b9e464
--- /dev/null
+++ b/android/arch/persistence/room/migration/bundle/PrimaryKeyBundleTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017 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.arch.persistence.room.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class PrimaryKeyBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ PrimaryKeyBundle other = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffAutoGen_notEqual() {
+ PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ PrimaryKeyBundle other = new PrimaryKeyBundle(false,
+ Arrays.asList("foo", "bar"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffColumns_notEqual() {
+ PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "baz"));
+ PrimaryKeyBundle other = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffColumnOrder_notEqual() {
+ PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ PrimaryKeyBundle other = new PrimaryKeyBundle(true,
+ Arrays.asList("bar", "foo"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+}
diff --git a/android/arch/persistence/room/migration/bundle/SchemaBundle.java b/android/arch/persistence/room/migration/bundle/SchemaBundle.java
index d6171aa..af35e6f 100644
--- a/android/arch/persistence/room/migration/bundle/SchemaBundle.java
+++ b/android/arch/persistence/room/migration/bundle/SchemaBundle.java
@@ -37,7 +37,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class SchemaBundle {
+public class SchemaBundle implements SchemaEquality<SchemaBundle> {
@SerializedName("formatVersion")
private int mFormatVersion;
@@ -47,6 +47,7 @@
private static final Gson GSON;
private static final String CHARSET = "UTF-8";
public static final int LATEST_FORMAT = 1;
+
static {
GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
}
@@ -104,4 +105,9 @@
}
}
+ @Override
+ public boolean isSchemaEqual(SchemaBundle other) {
+ return SchemaEqualityUtil.checkSchemaEquality(mDatabase, other.mDatabase)
+ && mFormatVersion == other.mFormatVersion;
+ }
}
diff --git a/android/arch/persistence/room/migration/bundle/SchemaEquality.java b/android/arch/persistence/room/migration/bundle/SchemaEquality.java
new file mode 100644
index 0000000..59ea4b0
--- /dev/null
+++ b/android/arch/persistence/room/migration/bundle/SchemaEquality.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 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.arch.persistence.room.migration.bundle;
+
+import android.support.annotation.RestrictTo;
+
+/**
+ * A loose equals check which checks schema equality instead of 100% equality (e.g. order of
+ * columns in an entity does not have to match)
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+interface SchemaEquality<T> {
+ boolean isSchemaEqual(T other);
+}
diff --git a/android/arch/persistence/room/migration/bundle/SchemaEqualityUtil.java b/android/arch/persistence/room/migration/bundle/SchemaEqualityUtil.java
new file mode 100644
index 0000000..65a7572
--- /dev/null
+++ b/android/arch/persistence/room/migration/bundle/SchemaEqualityUtil.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2017 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.arch.persistence.room.migration.bundle;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * utility class to run schema equality on collections.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class SchemaEqualityUtil {
+ static <T, K extends SchemaEquality<K>> boolean checkSchemaEquality(
+ @Nullable Map<T, K> map1, @Nullable Map<T, K> map2) {
+ if (map1 == null) {
+ return map2 == null;
+ }
+ if (map2 == null) {
+ return false;
+ }
+ if (map1.size() != map2.size()) {
+ return false;
+ }
+ for (Map.Entry<T, K> pair : map1.entrySet()) {
+ if (!checkSchemaEquality(pair.getValue(), map2.get(pair.getKey()))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static <K extends SchemaEquality<K>> boolean checkSchemaEquality(
+ @Nullable List<K> list1, @Nullable List<K> list2) {
+ if (list1 == null) {
+ return list2 == null;
+ }
+ if (list2 == null) {
+ return false;
+ }
+ if (list1.size() != list2.size()) {
+ return false;
+ }
+ // we don't care this is n^2, small list + only used for testing.
+ for (K item1 : list1) {
+ // find matching item
+ boolean matched = false;
+ for (K item2 : list2) {
+ if (checkSchemaEquality(item1, item2)) {
+ matched = true;
+ break;
+ }
+ }
+ if (!matched) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ static <K extends SchemaEquality<K>> boolean checkSchemaEquality(
+ @Nullable K item1, @Nullable K item2) {
+ if (item1 == null) {
+ return item2 == null;
+ }
+ if (item2 == null) {
+ return false;
+ }
+ return item1.isSchemaEqual(item2);
+ }
+}
diff --git a/android/arch/persistence/room/paging/LimitOffsetDataSource.java b/android/arch/persistence/room/paging/LimitOffsetDataSource.java
index 2f9a888..baa5b43 100644
--- a/android/arch/persistence/room/paging/LimitOffsetDataSource.java
+++ b/android/arch/persistence/room/paging/LimitOffsetDataSource.java
@@ -16,7 +16,7 @@
package android.arch.persistence.room.paging;
-import android.arch.paging.TiledDataSource;
+import android.arch.paging.PositionalDataSource;
import android.arch.persistence.room.InvalidationTracker;
import android.arch.persistence.room.RoomDatabase;
import android.arch.persistence.room.RoomSQLiteQuery;
@@ -25,6 +25,7 @@
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
+import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -42,7 +43,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class LimitOffsetDataSource<T> extends TiledDataSource<T> {
+public abstract class LimitOffsetDataSource<T> extends PositionalDataSource<T> {
private final RoomSQLiteQuery mSourceQuery;
private final String mCountQuery;
private final String mLimitOffsetQuery;
@@ -67,7 +68,10 @@
db.getInvalidationTracker().addWeakObserver(mObserver);
}
- @Override
+ /**
+ * Count number of rows query can return
+ */
+ @SuppressWarnings("WeakerAccess")
public int countItems() {
final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mCountQuery,
mSourceQuery.getArgCount());
@@ -93,8 +97,43 @@
@SuppressWarnings("WeakerAccess")
protected abstract List<T> convertRows(Cursor cursor);
- @Nullable
@Override
+ public void loadInitial(@NonNull LoadInitialParams params,
+ @NonNull LoadInitialCallback<T> callback) {
+ int totalCount = countItems();
+ if (totalCount == 0) {
+ callback.onResult(Collections.<T>emptyList(), 0, 0);
+ return;
+ }
+
+ // bound the size requested, based on known count
+ final int firstLoadPosition = computeInitialLoadPosition(params, totalCount);
+ final int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount);
+
+ List<T> list = loadRange(firstLoadPosition, firstLoadSize);
+ if (list != null && list.size() == firstLoadSize) {
+ callback.onResult(list, firstLoadPosition, totalCount);
+ } else {
+ // null list, or size doesn't match request - DB modified between count and load
+ invalidate();
+ }
+ }
+
+ @Override
+ public void loadRange(@NonNull LoadRangeParams params,
+ @NonNull LoadRangeCallback<T> callback) {
+ List<T> list = loadRange(params.startPosition, params.loadSize);
+ if (list != null) {
+ callback.onResult(list);
+ } else {
+ invalidate();
+ }
+ }
+
+ /**
+ * Return the rows from startPos to startPos + loadCount
+ */
+ @Nullable
public List<T> loadRange(int startPosition, int loadCount) {
final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mLimitOffsetQuery,
mSourceQuery.getArgCount() + 2);
diff --git a/android/arch/persistence/room/testing/MigrationTestHelper.java b/android/arch/persistence/room/testing/MigrationTestHelper.java
index 2e93bbe..013dd37 100644
--- a/android/arch/persistence/room/testing/MigrationTestHelper.java
+++ b/android/arch/persistence/room/testing/MigrationTestHelper.java
@@ -143,9 +143,12 @@
RoomDatabase.MigrationContainer container = new RoomDatabase.MigrationContainer();
DatabaseConfiguration configuration = new DatabaseConfiguration(
mInstrumentation.getTargetContext(), name, mOpenFactory, container, null, true,
- true);
+ true, Collections.<Integer>emptySet());
RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
new CreatingDelegate(schemaBundle.getDatabase()),
+ schemaBundle.getDatabase().getIdentityHash(),
+ // we pass the same hash twice since an old schema does not necessarily have
+ // a legacy hash and we would not even persist it.
schemaBundle.getDatabase().getIdentityHash());
return openDatabase(name, roomOpenHelper);
}
@@ -186,9 +189,12 @@
container.addMigrations(migrations);
DatabaseConfiguration configuration = new DatabaseConfiguration(
mInstrumentation.getTargetContext(), name, mOpenFactory, container, null, true,
- true);
+ true, Collections.<Integer>emptySet());
RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
new MigratingDelegate(schemaBundle.getDatabase(), validateDroppedTables),
+ // we pass the same hash twice since an old schema does not necessarily have
+ // a legacy hash and we would not even persist it.
+ schemaBundle.getDatabase().getIdentityHash(),
schemaBundle.getDatabase().getIdentityHash());
return openDatabase(name, roomOpenHelper);
}
diff --git a/android/arch/persistence/room/util/TableInfo.java b/android/arch/persistence/room/util/TableInfo.java
index a115147..19d9853 100644
--- a/android/arch/persistence/room/util/TableInfo.java
+++ b/android/arch/persistence/room/util/TableInfo.java
@@ -17,6 +17,7 @@
package android.arch.persistence.room.util;
import android.arch.persistence.db.SupportSQLiteDatabase;
+import android.arch.persistence.room.ColumnInfo;
import android.database.Cursor;
import android.os.Build;
import android.support.annotation.NonNull;
@@ -28,6 +29,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
@@ -44,7 +46,8 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources"})
+@SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources",
+ "SimplifiableIfStatement"})
// if you change this class, you must change TableInfoWriter.kt
public class TableInfo {
/**
@@ -313,6 +316,14 @@
*/
public final String type;
/**
+ * The column type after it is normalized to one of the basic types according to
+ * https://www.sqlite.org/datatype3.html Section 3.1.
+ * <p>
+ * This is the value Room uses for equality check.
+ */
+ @ColumnInfo.SQLiteTypeAffinity
+ public final int affinity;
+ /**
* Whether or not the column can be NULL.
*/
public final boolean notNull;
@@ -337,6 +348,40 @@
this.type = type;
this.notNull = notNull;
this.primaryKeyPosition = primaryKeyPosition;
+ this.affinity = findAffinity(type);
+ }
+
+ /**
+ * Implements https://www.sqlite.org/datatype3.html section 3.1
+ *
+ * @param type The type that was given to the sqlite
+ * @return The normalized type which is one of the 5 known affinities
+ */
+ @ColumnInfo.SQLiteTypeAffinity
+ private static int findAffinity(@Nullable String type) {
+ if (type == null) {
+ return ColumnInfo.BLOB;
+ }
+ String uppercaseType = type.toUpperCase(Locale.US);
+ if (uppercaseType.contains("INT")) {
+ return ColumnInfo.INTEGER;
+ }
+ if (uppercaseType.contains("CHAR")
+ || uppercaseType.contains("CLOB")
+ || uppercaseType.contains("TEXT")) {
+ return ColumnInfo.TEXT;
+ }
+ if (uppercaseType.contains("BLOB")) {
+ return ColumnInfo.BLOB;
+ }
+ if (uppercaseType.contains("REAL")
+ || uppercaseType.contains("FLOA")
+ || uppercaseType.contains("DOUB")) {
+ return ColumnInfo.REAL;
+ }
+ // sqlite returns NUMERIC here but it is like a catch all. We already
+ // have UNDEFINED so it is better to use UNDEFINED for consistency.
+ return ColumnInfo.UNDEFINED;
}
@Override
@@ -354,7 +399,7 @@
if (!name.equals(column.name)) return false;
//noinspection SimplifiableIfStatement
if (notNull != column.notNull) return false;
- return type != null ? type.equalsIgnoreCase(column.type) : column.type == null;
+ return affinity == column.affinity;
}
/**
@@ -369,7 +414,7 @@
@Override
public int hashCode() {
int result = name.hashCode();
- result = 31 * result + (type != null ? type.hashCode() : 0);
+ result = 31 * result + affinity;
result = 31 * result + (notNull ? 1231 : 1237);
result = 31 * result + primaryKeyPosition;
return result;
@@ -380,6 +425,7 @@
return "Column{"
+ "name='" + name + '\''
+ ", type='" + type + '\''
+ + ", affinity='" + affinity + '\''
+ ", notNull=" + notNull
+ ", primaryKeyPosition=" + primaryKeyPosition
+ '}';
@@ -472,7 +518,7 @@
}
@Override
- public int compareTo(ForeignKeyWithSequence o) {
+ public int compareTo(@NonNull ForeignKeyWithSequence o) {
final int idCmp = mId - o.mId;
if (idCmp == 0) {
return mSequence - o.mSequence;
diff --git a/android/bluetooth/BluetoothA2dp.java b/android/bluetooth/BluetoothA2dp.java
index 7841b83..35a21a4 100644
--- a/android/bluetooth/BluetoothA2dp.java
+++ b/android/bluetooth/BluetoothA2dp.java
@@ -17,6 +17,7 @@
package android.bluetooth;
import android.Manifest;
+import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
@@ -103,6 +104,24 @@
"android.bluetooth.a2dp.profile.action.AVRCP_CONNECTION_STATE_CHANGED";
/**
+ * Intent used to broadcast the selection of a connected device as active.
+ *
+ * <p>This intent will have one extra:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can
+ * be null if no device is active. </li>
+ * </ul>
+ *
+ * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to
+ * receive.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_ACTIVE_DEVICE_CHANGED =
+ "android.bluetooth.a2dp.profile.action.ACTIVE_DEVICE_CHANGED";
+
+ /**
* Intent used to broadcast the change in the Audio Codec state of the
* A2DP Source profile.
*
@@ -425,6 +444,75 @@
}
/**
+ * Select a connected device as active.
+ *
+ * The active device selection is per profile. An active device's
+ * purpose is profile-specific. For example, A2DP audio streaming
+ * is to the active A2DP Sink device. If a remote device is not
+ * connected, it cannot be selected as active.
+ *
+ * <p> This API returns false in scenarios like the profile on the
+ * device is not connected or Bluetooth is not turned on.
+ * When this API returns true, it is guaranteed that the
+ * {@link #ACTION_ACTIVE_DEVICE_CHANGED} intent will be broadcasted
+ * with the active device.
+ *
+ * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
+ * permission.
+ *
+ * @param device the remote Bluetooth device. Could be null to clear
+ * the active device and stop streaming audio to a Bluetooth device.
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ public boolean setActiveDevice(@Nullable BluetoothDevice device) {
+ if (DBG) log("setActiveDevice(" + device + ")");
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null && isEnabled()
+ && ((device == null) || isValidDevice(device))) {
+ return mService.setActiveDevice(device);
+ }
+ if (mService == null) Log.w(TAG, "Proxy not attached to service");
+ return false;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return false;
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Get the connected device that is active.
+ *
+ * <p>Requires {@link android.Manifest.permission#BLUETOOTH}
+ * permission.
+ *
+ * @return the connected device that is active or null if no device
+ * is active
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH)
+ @Nullable
+ public BluetoothDevice getActiveDevice() {
+ if (VDBG) log("getActiveDevice()");
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null && isEnabled()) {
+ return mService.getActiveDevice();
+ }
+ if (mService == null) Log.w(TAG, "Proxy not attached to service");
+ return null;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return null;
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ }
+
+ /**
* Set priority of the profile
*
* <p> The device should already be paired.
diff --git a/android/bluetooth/BluetoothAdapter.java b/android/bluetooth/BluetoothAdapter.java
index 3290d57..9f11d6e 100644
--- a/android/bluetooth/BluetoothAdapter.java
+++ b/android/bluetooth/BluetoothAdapter.java
@@ -79,8 +79,9 @@
* {@link BluetoothDevice} objects representing all paired devices with
* {@link #getBondedDevices()}; start device discovery with
* {@link #startDiscovery()}; or create a {@link BluetoothServerSocket} to
- * listen for incoming connection requests with
- * {@link #listenUsingRfcommWithServiceRecord(String, UUID)}; or start a scan for
+ * listen for incoming RFComm connection requests with {@link
+ * #listenUsingRfcommWithServiceRecord(String, UUID)}; listen for incoming L2CAP Connection-oriented
+ * Channels (CoC) connection requests with listenUsingL2capCoc(int)}; or start a scan for
* Bluetooth LE devices with {@link #startLeScan(LeScanCallback callback)}.
* </p>
* <p>This class is thread safe.</p>
@@ -210,6 +211,14 @@
public static final int STATE_BLE_TURNING_OFF = 16;
/**
+ * UUID of the GATT Read Characteristics for LE_PSM value.
+ *
+ * @hide
+ */
+ public static final UUID LE_PSM_CHARACTERISTIC_UUID =
+ UUID.fromString("2d410339-82b6-42aa-b34e-e2e01df8cc1a");
+
+ /**
* Human-readable string helper for AdapterState
*
* @hide
@@ -1675,6 +1684,27 @@
}
/**
+ * Get the maximum number of connected audio devices.
+ *
+ * @return the maximum number of connected audio devices
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH)
+ public int getMaxConnectedAudioDevices() {
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.getMaxConnectedAudioDevices();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to get getMaxConnectedAudioDevices, error: ", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return 1;
+ }
+
+ /**
* Return true if hardware has entries available for matching beacons
*
* @return true if there are hw entries available for matching beacons
@@ -2139,7 +2169,9 @@
min16DigitPin);
int errno = socket.mSocket.bindListen();
if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
- socket.setChannel(socket.mSocket.getPort());
+ int assignedChannel = socket.mSocket.getPort();
+ if (DBG) Log.d(TAG, "listenUsingL2capOn: set assigned channel to " + assignedChannel);
+ socket.setChannel(assignedChannel);
}
if (errno != 0) {
//TODO(BT): Throw the same exception error code
@@ -2180,12 +2212,18 @@
* @hide
*/
public BluetoothServerSocket listenUsingInsecureL2capOn(int port) throws IOException {
+ Log.d(TAG, "listenUsingInsecureL2capOn: port=" + port);
BluetoothServerSocket socket =
new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP, false, false, port, false,
- false);
+ false);
int errno = socket.mSocket.bindListen();
if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
- socket.setChannel(socket.mSocket.getPort());
+ int assignedChannel = socket.mSocket.getPort();
+ if (DBG) {
+ Log.d(TAG, "listenUsingInsecureL2capOn: set assigned channel to "
+ + assignedChannel);
+ }
+ socket.setChannel(assignedChannel);
}
if (errno != 0) {
//TODO(BT): Throw the same exception error code
@@ -2744,4 +2782,103 @@
scanner.stopScan(scanCallback);
}
}
+
+ /**
+ * Create a secure L2CAP Connection-oriented Channel (CoC) {@link BluetoothServerSocket} and
+ * assign a dynamic protocol/service multiplexer (PSM) value. This socket can be used to listen
+ * for incoming connections.
+ * <p>A remote device connecting to this socket will be authenticated and communication on this
+ * socket will be encrypted.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming connections from a listening
+ * {@link BluetoothServerSocket}.
+ * <p>The system will assign a dynamic PSM value. This PSM value can be read from the {#link
+ * BluetoothServerSocket#getPsm()} and this value will be released when this server socket is
+ * closed, Bluetooth is turned off, or the application exits unexpectedly.
+ * <p>The mechanism of disclosing the assigned dynamic PSM value to the initiating peer is
+ * defined and performed by the application.
+ * <p>Use {@link BluetoothDevice#createL2capCocSocket(int, int)} to connect to this server
+ * socket from another Android device that is given the PSM value.
+ *
+ * @param transport Bluetooth transport to use, must be {@link BluetoothDevice#TRANSPORT_LE}
+ * @return an L2CAP CoC BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or unable to start this CoC
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH)
+ public BluetoothServerSocket listenUsingL2capCoc(int transport)
+ throws IOException {
+ if (transport != BluetoothDevice.TRANSPORT_LE) {
+ throw new IllegalArgumentException("Unsupported transport: " + transport);
+ }
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP_LE, true, true,
+ SOCKET_CHANNEL_AUTO_STATIC_NO_SDP, false, false);
+ int errno = socket.mSocket.bindListen();
+ if (errno != 0) {
+ throw new IOException("Error: " + errno);
+ }
+
+ int assignedPsm = socket.mSocket.getPort();
+ if (assignedPsm == 0) {
+ throw new IOException("Error: Unable to assign PSM value");
+ }
+ if (DBG) {
+ Log.d(TAG, "listenUsingL2capCoc: set assigned PSM to "
+ + assignedPsm);
+ }
+ socket.setChannel(assignedPsm);
+
+ return socket;
+ }
+
+ /**
+ * Create an insecure L2CAP Connection-oriented Channel (CoC) {@link BluetoothServerSocket} and
+ * assign a dynamic PSM value. This socket can be used to listen for incoming connections.
+ * <p>The link key is not required to be authenticated, i.e the communication may be vulnerable
+ * to man-in-the-middle attacks. Use {@link #listenUsingL2capCoc}, if an encrypted and
+ * authenticated communication channel is desired.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming connections from a listening
+ * {@link BluetoothServerSocket}.
+ * <p>The system will assign a dynamic protocol/service multiplexer (PSM) value. This PSM value
+ * can be read from the {#link BluetoothServerSocket#getPsm()} and this value will be released
+ * when this server socket is closed, Bluetooth is turned off, or the application exits
+ * unexpectedly.
+ * <p>The mechanism of disclosing the assigned dynamic PSM value to the initiating peer is
+ * defined and performed by the application.
+ * <p>Use {@link BluetoothDevice#createInsecureL2capCocSocket(int, int)} to connect to this
+ * server socket from another Android device that is given the PSM value.
+ *
+ * @param transport Bluetooth transport to use, must be {@link BluetoothDevice#TRANSPORT_LE}
+ * @return an L2CAP CoC BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or unable to start this CoC
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH)
+ public BluetoothServerSocket listenUsingInsecureL2capCoc(int transport)
+ throws IOException {
+ if (transport != BluetoothDevice.TRANSPORT_LE) {
+ throw new IllegalArgumentException("Unsupported transport: " + transport);
+ }
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP_LE, false, false,
+ SOCKET_CHANNEL_AUTO_STATIC_NO_SDP, false, false);
+ int errno = socket.mSocket.bindListen();
+ if (errno != 0) {
+ throw new IOException("Error: " + errno);
+ }
+
+ int assignedPsm = socket.mSocket.getPort();
+ if (assignedPsm == 0) {
+ throw new IOException("Error: Unable to assign PSM value");
+ }
+ if (DBG) {
+ Log.d(TAG, "listenUsingInsecureL2capOn: set assigned PSM to "
+ + assignedPsm);
+ }
+ socket.setChannel(assignedPsm);
+
+ return socket;
+ }
}
diff --git a/android/bluetooth/BluetoothDevice.java b/android/bluetooth/BluetoothDevice.java
index ad7a93c..ac21395 100644
--- a/android/bluetooth/BluetoothDevice.java
+++ b/android/bluetooth/BluetoothDevice.java
@@ -618,6 +618,7 @@
*
* @hide
*/
+ @SystemApi
public static final int ACCESS_UNKNOWN = 0;
/**
@@ -626,6 +627,7 @@
*
* @hide
*/
+ @SystemApi
public static final int ACCESS_ALLOWED = 1;
/**
@@ -634,6 +636,7 @@
*
* @hide
*/
+ @SystemApi
public static final int ACCESS_REJECTED = 2;
/**
@@ -1918,4 +1921,75 @@
}
return null;
}
+
+ /**
+ * Create a Bluetooth L2CAP Connection-oriented Channel (CoC) {@link BluetoothSocket} that can
+ * be used to start a secure outgoing connection to the remote device with the same dynamic
+ * protocol/service multiplexer (PSM) value.
+ * <p>This is designed to be used with {@link BluetoothAdapter#listenUsingL2capCoc(int)} for
+ * peer-peer Bluetooth applications.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing connection.
+ * <p>Application using this API is responsible for obtaining PSM value from remote device.
+ * <p>The remote device will be authenticated and communication on this socket will be
+ * encrypted.
+ * <p> Use this socket if an authenticated socket link is possible. Authentication refers
+ * to the authentication of the link key to prevent man-in-the-middle type of attacks. When a
+ * secure socket connection is not possible, use {#link createInsecureLeL2capCocSocket(int,
+ * int)}.
+ *
+ * @param transport Bluetooth transport to use, must be {@link #TRANSPORT_LE}
+ * @param psm dynamic PSM value from remote device
+ * @return a CoC #BluetoothSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH)
+ public BluetoothSocket createL2capCocSocket(int transport, int psm) throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "createL2capCocSocket: Bluetooth is not enabled");
+ throw new IOException();
+ }
+ if (transport != BluetoothDevice.TRANSPORT_LE) {
+ throw new IllegalArgumentException("Unsupported transport: " + transport);
+ }
+ if (DBG) Log.d(TAG, "createL2capCocSocket: transport=" + transport + ", psm=" + psm);
+ return new BluetoothSocket(BluetoothSocket.TYPE_L2CAP_LE, -1, true, true, this, psm,
+ null);
+ }
+
+ /**
+ * Create a Bluetooth L2CAP Connection-oriented Channel (CoC) {@link BluetoothSocket} that can
+ * be used to start a secure outgoing connection to the remote device with the same dynamic
+ * protocol/service multiplexer (PSM) value.
+ * <p>This is designed to be used with {@link BluetoothAdapter#listenUsingInsecureL2capCoc(int)}
+ * for peer-peer Bluetooth applications.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing connection.
+ * <p>Application using this API is responsible for obtaining PSM value from remote device.
+ * <p> The communication channel may not have an authenticated link key, i.e. it may be subject
+ * to man-in-the-middle attacks. Use {@link #createL2capCocSocket(int, int)} if an encrypted and
+ * authenticated communication channel is possible.
+ *
+ * @param transport Bluetooth transport to use, must be {@link #TRANSPORT_LE}
+ * @param psm dynamic PSM value from remote device
+ * @return a CoC #BluetoothSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH)
+ public BluetoothSocket createInsecureL2capCocSocket(int transport, int psm) throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "createInsecureL2capCocSocket: Bluetooth is not enabled");
+ throw new IOException();
+ }
+ if (transport != BluetoothDevice.TRANSPORT_LE) {
+ throw new IllegalArgumentException("Unsupported transport: " + transport);
+ }
+ if (DBG) {
+ Log.d(TAG, "createInsecureL2capCocSocket: transport=" + transport + ", psm=" + psm);
+ }
+ return new BluetoothSocket(BluetoothSocket.TYPE_L2CAP_LE, -1, false, false, this, psm,
+ null);
+ }
}
diff --git a/android/bluetooth/BluetoothHeadset.java b/android/bluetooth/BluetoothHeadset.java
index 838d315..a68f485 100644
--- a/android/bluetooth/BluetoothHeadset.java
+++ b/android/bluetooth/BluetoothHeadset.java
@@ -16,6 +16,7 @@
package android.bluetooth;
+import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
@@ -93,6 +94,23 @@
public static final String ACTION_AUDIO_STATE_CHANGED =
"android.bluetooth.headset.profile.action.AUDIO_STATE_CHANGED";
+ /**
+ * Intent used to broadcast the selection of a connected device as active.
+ *
+ * <p>This intent will have one extra:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can
+ * be null if no device is active. </li>
+ * </ul>
+ *
+ * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to
+ * receive.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_ACTIVE_DEVICE_CHANGED =
+ "android.bluetooth.headset.profile.action.ACTIVE_DEVICE_CHANGED";
/**
* Intent used to broadcast that the headset has posted a
@@ -538,8 +556,8 @@
* Set priority of the profile
*
* <p> The device should already be paired.
- * Priority can be one of {@link #PRIORITY_ON} or
- * {@link #PRIORITY_OFF},
+ * Priority can be one of {@link BluetoothProfile#PRIORITY_ON} or
+ * {@link BluetoothProfile#PRIORITY_OFF},
*
* <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
* permission.
@@ -983,9 +1001,105 @@
}
/**
- * check if in-band ringing is supported for this platform.
+ * Select a connected device as active.
*
- * @return true if in-band ringing is supported false if in-band ringing is not supported
+ * The active device selection is per profile. An active device's
+ * purpose is profile-specific. For example, in HFP and HSP profiles,
+ * it is the device used for phone call audio. If a remote device is not
+ * connected, it cannot be selected as active.
+ *
+ * <p> This API returns false in scenarios like the profile on the
+ * device is not connected or Bluetooth is not turned on.
+ * When this API returns true, it is guaranteed that the
+ * {@link #ACTION_ACTIVE_DEVICE_CHANGED} intent will be broadcasted
+ * with the active device.
+ *
+ * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
+ * permission.
+ *
+ * @param device Remote Bluetooth Device, could be null if phone call audio should not be
+ * streamed to a headset
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN)
+ public boolean setActiveDevice(@Nullable BluetoothDevice device) {
+ if (DBG) {
+ Log.d(TAG, "setActiveDevice: " + device);
+ }
+ final IBluetoothHeadset service = mService;
+ if (service != null && isEnabled() && (device == null || isValidDevice(device))) {
+ try {
+ return service.setActiveDevice(device);
+ } catch (RemoteException e) {
+ Log.e(TAG, Log.getStackTraceString(new Throwable()));
+ }
+ }
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ }
+ return false;
+ }
+
+ /**
+ * Get the connected device that is active.
+ *
+ * <p>Requires {@link android.Manifest.permission#BLUETOOTH}
+ * permission.
+ *
+ * @return the connected device that is active or null if no device
+ * is active.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH)
+ public BluetoothDevice getActiveDevice() {
+ if (VDBG) {
+ Log.d(TAG, "getActiveDevice");
+ }
+ final IBluetoothHeadset service = mService;
+ if (service != null && isEnabled()) {
+ try {
+ return service.getActiveDevice();
+ } catch (RemoteException e) {
+ Log.e(TAG, Log.getStackTraceString(new Throwable()));
+ }
+ }
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ }
+ return null;
+ }
+
+ /**
+ * Check if in-band ringing is currently enabled. In-band ringing could be disabled during an
+ * active connection.
+ *
+ * @return true if in-band ringing is enabled, false if in-band ringing is disabled
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH)
+ public boolean isInbandRingingEnabled() {
+ if (DBG) {
+ log("isInbandRingingEnabled()");
+ }
+ final IBluetoothHeadset service = mService;
+ if (service != null && isEnabled()) {
+ try {
+ return service.isInbandRingingEnabled();
+ } catch (RemoteException e) {
+ Log.e(TAG, Log.getStackTraceString(new Throwable()));
+ }
+ }
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ }
+ return false;
+ }
+
+ /**
+ * Check if in-band ringing is supported for this platform.
+ *
+ * @return true if in-band ringing is supported, false if in-band ringing is not supported
* @hide
*/
public static boolean isInbandRingingSupported(Context context) {
diff --git a/android/bluetooth/BluetoothHeadsetClientCall.java b/android/bluetooth/BluetoothHeadsetClientCall.java
index dc00d63..d46b2e3 100644
--- a/android/bluetooth/BluetoothHeadsetClientCall.java
+++ b/android/bluetooth/BluetoothHeadsetClientCall.java
@@ -73,17 +73,18 @@
private final boolean mOutgoing;
private final UUID mUUID;
private final long mCreationElapsedMilli;
+ private final boolean mInBandRing;
/**
* Creates BluetoothHeadsetClientCall instance.
*/
public BluetoothHeadsetClientCall(BluetoothDevice device, int id, int state, String number,
- boolean multiParty, boolean outgoing) {
- this(device, id, UUID.randomUUID(), state, number, multiParty, outgoing);
+ boolean multiParty, boolean outgoing, boolean inBandRing) {
+ this(device, id, UUID.randomUUID(), state, number, multiParty, outgoing, inBandRing);
}
public BluetoothHeadsetClientCall(BluetoothDevice device, int id, UUID uuid, int state,
- String number, boolean multiParty, boolean outgoing) {
+ String number, boolean multiParty, boolean outgoing, boolean inBandRing) {
mDevice = device;
mId = id;
mUUID = uuid;
@@ -91,6 +92,7 @@
mNumber = number != null ? number : "";
mMultiParty = multiParty;
mOutgoing = outgoing;
+ mInBandRing = inBandRing;
mCreationElapsedMilli = SystemClock.elapsedRealtime();
}
@@ -200,6 +202,16 @@
return mOutgoing;
}
+ /**
+ * Checks if the ringtone will be generated by the connected phone
+ *
+ * @return <code>true</code> if in band ring is enabled, <code>false</code> otherwise.
+ */
+ public boolean isInBandRing() {
+ return mInBandRing;
+ }
+
+
@Override
public String toString() {
return toString(false);
@@ -253,6 +265,8 @@
builder.append(mMultiParty);
builder.append(", mOutgoing: ");
builder.append(mOutgoing);
+ builder.append(", mInBandRing: ");
+ builder.append(mInBandRing);
builder.append("}");
return builder.toString();
}
@@ -266,7 +280,8 @@
public BluetoothHeadsetClientCall createFromParcel(Parcel in) {
return new BluetoothHeadsetClientCall((BluetoothDevice) in.readParcelable(null),
in.readInt(), UUID.fromString(in.readString()), in.readInt(),
- in.readString(), in.readInt() == 1, in.readInt() == 1);
+ in.readString(), in.readInt() == 1, in.readInt() == 1,
+ in.readInt() == 1);
}
@Override
@@ -284,6 +299,7 @@
out.writeString(mNumber);
out.writeInt(mMultiParty ? 1 : 0);
out.writeInt(mOutgoing ? 1 : 0);
+ out.writeInt(mInBandRing ? 1 : 0);
}
@Override
diff --git a/android/bluetooth/BluetoothProfile.java b/android/bluetooth/BluetoothProfile.java
index df2028a..0e2263f 100644
--- a/android/bluetooth/BluetoothProfile.java
+++ b/android/bluetooth/BluetoothProfile.java
@@ -19,6 +19,7 @@
import android.Manifest;
import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import java.util.List;
@@ -157,12 +158,19 @@
public static final int HID_DEVICE = 19;
/**
+ * Object Push Profile (OPP)
+ *
+ * @hide
+ */
+ public static final int OPP = 20;
+
+ /**
* Max profile ID. This value should be updated whenever a new profile is added to match
* the largest value assigned to a profile.
*
* @hide
*/
- public static final int MAX_PROFILE_ID = 19;
+ public static final int MAX_PROFILE_ID = 20;
/**
* Default priority for devices that we try to auto-connect to and
@@ -178,6 +186,7 @@
*
* @hide
**/
+ @SystemApi
public static final int PRIORITY_ON = 100;
/**
@@ -186,6 +195,7 @@
*
* @hide
**/
+ @SystemApi
public static final int PRIORITY_OFF = 0;
/**
diff --git a/android/bluetooth/BluetoothServerSocket.java b/android/bluetooth/BluetoothServerSocket.java
index 58d090d..ebb7f18 100644
--- a/android/bluetooth/BluetoothServerSocket.java
+++ b/android/bluetooth/BluetoothServerSocket.java
@@ -68,6 +68,7 @@
public final class BluetoothServerSocket implements Closeable {
private static final String TAG = "BluetoothServerSocket";
+ private static final boolean DBG = false;
/*package*/ final BluetoothSocket mSocket;
private Handler mHandler;
private int mMessage;
@@ -169,6 +170,7 @@
* close any {@link BluetoothSocket} received from {@link #accept()}.
*/
public void close() throws IOException {
+ if (DBG) Log.d(TAG, "BluetoothServerSocket:close() called. mChannel=" + mChannel);
synchronized (this) {
if (mHandler != null) {
mHandler.obtainMessage(mMessage).sendToTarget();
@@ -197,6 +199,20 @@
}
/**
+ * Returns the assigned dynamic protocol/service multiplexer (PSM) value for the listening L2CAP
+ * Connection-oriented Channel (CoC) server socket. This server socket must be returned by the
+ * {#link BluetoothAdapter.listenUsingL2capCoc(int)} or {#link
+ * BluetoothAdapter.listenUsingInsecureL2capCoc(int)}. The returned value is undefined if this
+ * method is called on non-L2CAP server sockets.
+ *
+ * @return the assigned PSM or LE_PSM value depending on transport
+ * @hide
+ */
+ public int getPsm() {
+ return mChannel;
+ }
+
+ /**
* Sets the channel on which future sockets are bound.
* Currently used only when a channel is auto generated.
*/
@@ -227,6 +243,10 @@
sb.append("TYPE_L2CAP");
break;
}
+ case BluetoothSocket.TYPE_L2CAP_LE: {
+ sb.append("TYPE_L2CAP_LE");
+ break;
+ }
case BluetoothSocket.TYPE_SCO: {
sb.append("TYPE_SCO");
break;
diff --git a/android/bluetooth/BluetoothSocket.java b/android/bluetooth/BluetoothSocket.java
index 0569913..09f9684 100644
--- a/android/bluetooth/BluetoothSocket.java
+++ b/android/bluetooth/BluetoothSocket.java
@@ -99,6 +99,16 @@
/** L2CAP socket */
public static final int TYPE_L2CAP = 3;
+ /** L2CAP socket on BR/EDR transport
+ * @hide
+ */
+ public static final int TYPE_L2CAP_BREDR = TYPE_L2CAP;
+
+ /** L2CAP socket on LE transport
+ * @hide
+ */
+ public static final int TYPE_L2CAP_LE = 4;
+
/*package*/ static final int EBADFD = 77;
/*package*/ static final int EADDRINUSE = 98;
@@ -417,6 +427,7 @@
return -1;
}
try {
+ if (DBG) Log.d(TAG, "bindListen(): mPort=" + mPort + ", mType=" + mType);
mPfd = bluetoothProxy.getSocketManager().createSocketChannel(mType, mServiceName,
mUuid, mPort, getSecurityFlags());
} catch (RemoteException e) {
@@ -451,7 +462,7 @@
mSocketState = SocketState.LISTENING;
}
}
- if (DBG) Log.d(TAG, "channel: " + channel);
+ if (DBG) Log.d(TAG, "bindListen(): channel=" + channel + ", mPort=" + mPort);
if (mPort <= -1) {
mPort = channel;
} // else ASSERT(mPort == channel)
@@ -515,7 +526,7 @@
/*package*/ int read(byte[] b, int offset, int length) throws IOException {
int ret = 0;
if (VDBG) Log.d(TAG, "read in: " + mSocketIS + " len: " + length);
- if (mType == TYPE_L2CAP) {
+ if ((mType == TYPE_L2CAP) || (mType == TYPE_L2CAP_LE)) {
int bytesToRead = length;
if (VDBG) {
Log.v(TAG, "l2cap: read(): offset: " + offset + " length:" + length
@@ -558,7 +569,7 @@
// Rfcomm uses dynamic allocation, and should not have any bindings
// to the actual message length.
if (VDBG) Log.d(TAG, "write: " + mSocketOS + " length: " + length);
- if (mType == TYPE_L2CAP) {
+ if ((mType == TYPE_L2CAP) || (mType == TYPE_L2CAP_LE)) {
if (length <= mMaxTxPacketSize) {
mSocketOS.write(b, offset, length);
} else {
@@ -702,7 +713,7 @@
}
private void createL2capRxBuffer() {
- if (mType == TYPE_L2CAP) {
+ if ((mType == TYPE_L2CAP) || (mType == TYPE_L2CAP_LE)) {
// Allocate the buffer to use for reads.
if (VDBG) Log.v(TAG, " Creating mL2capBuffer: mMaxPacketSize: " + mMaxRxPacketSize);
mL2capBuffer = ByteBuffer.wrap(new byte[mMaxRxPacketSize]);
diff --git a/android/bluetooth/client/map/BluetoothMapBmessage.java b/android/bluetooth/client/map/BluetoothMapBmessage.java
deleted file mode 100644
index e06b033..0000000
--- a/android/bluetooth/client/map/BluetoothMapBmessage.java
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import com.android.vcard.VCardEntry;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.ArrayList;
-
-/**
- * Object representation of message in bMessage format
- * <p>
- * This object will be received in {@link BluetoothMasClient#EVENT_GET_MESSAGE}
- * callback message.
- */
-public class BluetoothMapBmessage {
-
- String mBmsgVersion;
- Status mBmsgStatus;
- Type mBmsgType;
- String mBmsgFolder;
-
- String mBbodyEncoding;
- String mBbodyCharset;
- String mBbodyLanguage;
- int mBbodyLength;
-
- String mMessage;
-
- ArrayList<VCardEntry> mOriginators;
- ArrayList<VCardEntry> mRecipients;
-
- public enum Status {
- READ, UNREAD
- }
-
- public enum Type {
- EMAIL, SMS_GSM, SMS_CDMA, MMS
- }
-
- /**
- * Constructs empty message object
- */
- public BluetoothMapBmessage() {
- mOriginators = new ArrayList<VCardEntry>();
- mRecipients = new ArrayList<VCardEntry>();
- }
-
- public VCardEntry getOriginator() {
- if (mOriginators.size() > 0) {
- return mOriginators.get(0);
- } else {
- return null;
- }
- }
-
- public ArrayList<VCardEntry> getOriginators() {
- return mOriginators;
- }
-
- public BluetoothMapBmessage addOriginator(VCardEntry vcard) {
- mOriginators.add(vcard);
- return this;
- }
-
- public ArrayList<VCardEntry> getRecipients() {
- return mRecipients;
- }
-
- public BluetoothMapBmessage addRecipient(VCardEntry vcard) {
- mRecipients.add(vcard);
- return this;
- }
-
- public Status getStatus() {
- return mBmsgStatus;
- }
-
- public BluetoothMapBmessage setStatus(Status status) {
- mBmsgStatus = status;
- return this;
- }
-
- public Type getType() {
- return mBmsgType;
- }
-
- public BluetoothMapBmessage setType(Type type) {
- mBmsgType = type;
- return this;
- }
-
- public String getFolder() {
- return mBmsgFolder;
- }
-
- public BluetoothMapBmessage setFolder(String folder) {
- mBmsgFolder = folder;
- return this;
- }
-
- public String getEncoding() {
- return mBbodyEncoding;
- }
-
- public BluetoothMapBmessage setEncoding(String encoding) {
- mBbodyEncoding = encoding;
- return this;
- }
-
- public String getCharset() {
- return mBbodyCharset;
- }
-
- public BluetoothMapBmessage setCharset(String charset) {
- mBbodyCharset = charset;
- return this;
- }
-
- public String getLanguage() {
- return mBbodyLanguage;
- }
-
- public BluetoothMapBmessage setLanguage(String language) {
- mBbodyLanguage = language;
- return this;
- }
-
- public String getBodyContent() {
- return mMessage;
- }
-
- public BluetoothMapBmessage setBodyContent(String body) {
- mMessage = body;
- return this;
- }
-
- @Override
- public String toString() {
- JSONObject json = new JSONObject();
-
- try {
- json.put("status", mBmsgStatus);
- json.put("type", mBmsgType);
- json.put("folder", mBmsgFolder);
- json.put("charset", mBbodyCharset);
- json.put("message", mMessage);
- } catch (JSONException e) {
- // do nothing
- }
-
- return json.toString();
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMapBmessageBuilder.java b/android/bluetooth/client/map/BluetoothMapBmessageBuilder.java
deleted file mode 100644
index 8629423..0000000
--- a/android/bluetooth/client/map/BluetoothMapBmessageBuilder.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-import com.android.vcard.VCardEntry;
-import com.android.vcard.VCardEntry.EmailData;
-import com.android.vcard.VCardEntry.NameData;
-import com.android.vcard.VCardEntry.PhoneData;
-
-import java.util.List;
-
-class BluetoothMapBmessageBuilder {
-
- private final static String CRLF = "\r\n";
-
- private final static String BMSG_BEGIN = "BEGIN:BMSG";
- private final static String BMSG_VERSION = "VERSION:1.0";
- private final static String BMSG_STATUS = "STATUS:";
- private final static String BMSG_TYPE = "TYPE:";
- private final static String BMSG_FOLDER = "FOLDER:";
- private final static String BMSG_END = "END:BMSG";
-
- private final static String BENV_BEGIN = "BEGIN:BENV";
- private final static String BENV_END = "END:BENV";
-
- private final static String BBODY_BEGIN = "BEGIN:BBODY";
- private final static String BBODY_ENCODING = "ENCODING:";
- private final static String BBODY_CHARSET = "CHARSET:";
- private final static String BBODY_LANGUAGE = "LANGUAGE:";
- private final static String BBODY_LENGTH = "LENGTH:";
- private final static String BBODY_END = "END:BBODY";
-
- private final static String MSG_BEGIN = "BEGIN:MSG";
- private final static String MSG_END = "END:MSG";
-
- private final static String VCARD_BEGIN = "BEGIN:VCARD";
- private final static String VCARD_VERSION = "VERSION:2.1";
- private final static String VCARD_N = "N:";
- private final static String VCARD_EMAIL = "EMAIL:";
- private final static String VCARD_TEL = "TEL:";
- private final static String VCARD_END = "END:VCARD";
-
- private final StringBuilder mBmsg;
-
- private BluetoothMapBmessageBuilder() {
- mBmsg = new StringBuilder();
- }
-
- static public String createBmessage(BluetoothMapBmessage bmsg) {
- BluetoothMapBmessageBuilder b = new BluetoothMapBmessageBuilder();
-
- b.build(bmsg);
-
- return b.mBmsg.toString();
- }
-
- private void build(BluetoothMapBmessage bmsg) {
- int bodyLen = MSG_BEGIN.length() + MSG_END.length() + 3 * CRLF.length()
- + bmsg.mMessage.getBytes().length;
-
- mBmsg.append(BMSG_BEGIN).append(CRLF);
-
- mBmsg.append(BMSG_VERSION).append(CRLF);
- mBmsg.append(BMSG_STATUS).append(bmsg.mBmsgStatus).append(CRLF);
- mBmsg.append(BMSG_TYPE).append(bmsg.mBmsgType).append(CRLF);
- mBmsg.append(BMSG_FOLDER).append(bmsg.mBmsgFolder).append(CRLF);
-
- for (VCardEntry vcard : bmsg.mOriginators) {
- buildVcard(vcard);
- }
-
- {
- mBmsg.append(BENV_BEGIN).append(CRLF);
-
- for (VCardEntry vcard : bmsg.mRecipients) {
- buildVcard(vcard);
- }
-
- {
- mBmsg.append(BBODY_BEGIN).append(CRLF);
-
- if (bmsg.mBbodyEncoding != null) {
- mBmsg.append(BBODY_ENCODING).append(bmsg.mBbodyEncoding).append(CRLF);
- }
-
- if (bmsg.mBbodyCharset != null) {
- mBmsg.append(BBODY_CHARSET).append(bmsg.mBbodyCharset).append(CRLF);
- }
-
- if (bmsg.mBbodyLanguage != null) {
- mBmsg.append(BBODY_LANGUAGE).append(bmsg.mBbodyLanguage).append(CRLF);
- }
-
- mBmsg.append(BBODY_LENGTH).append(bodyLen).append(CRLF);
-
- {
- mBmsg.append(MSG_BEGIN).append(CRLF);
-
- mBmsg.append(bmsg.mMessage).append(CRLF);
-
- mBmsg.append(MSG_END).append(CRLF);
- }
-
- mBmsg.append(BBODY_END).append(CRLF);
- }
-
- mBmsg.append(BENV_END).append(CRLF);
- }
-
- mBmsg.append(BMSG_END).append(CRLF);
- }
-
- private void buildVcard(VCardEntry vcard) {
- String n = buildVcardN(vcard);
- List<PhoneData> tel = vcard.getPhoneList();
- List<EmailData> email = vcard.getEmailList();
-
- mBmsg.append(VCARD_BEGIN).append(CRLF);
-
- mBmsg.append(VCARD_VERSION).append(CRLF);
-
- mBmsg.append(VCARD_N).append(n).append(CRLF);
-
- if (tel != null && tel.size() > 0) {
- mBmsg.append(VCARD_TEL).append(tel.get(0).getNumber()).append(CRLF);
- }
-
- if (email != null && email.size() > 0) {
- mBmsg.append(VCARD_EMAIL).append(email.get(0).getAddress()).append(CRLF);
- }
-
- mBmsg.append(VCARD_END).append(CRLF);
- }
-
- private String buildVcardN(VCardEntry vcard) {
- NameData nd = vcard.getNameData();
- StringBuilder sb = new StringBuilder();
-
- sb.append(nd.getFamily()).append(";");
- sb.append(nd.getGiven() == null ? "" : nd.getGiven()).append(";");
- sb.append(nd.getMiddle() == null ? "" : nd.getMiddle()).append(";");
- sb.append(nd.getPrefix() == null ? "" : nd.getPrefix()).append(";");
- sb.append(nd.getSuffix() == null ? "" : nd.getSuffix());
-
- return sb.toString();
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMapBmessageParser.java b/android/bluetooth/client/map/BluetoothMapBmessageParser.java
deleted file mode 100644
index ea9bc7f..0000000
--- a/android/bluetooth/client/map/BluetoothMapBmessageParser.java
+++ /dev/null
@@ -1,459 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import android.util.Log;
-
-import com.android.vcard.VCardEntry;
-import com.android.vcard.VCardEntryConstructor;
-import com.android.vcard.VCardEntryHandler;
-import com.android.vcard.VCardParser;
-import com.android.vcard.VCardParser_V21;
-import com.android.vcard.VCardParser_V30;
-import com.android.vcard.exception.VCardException;
-import com.android.vcard.exception.VCardVersionException;
-import android.bluetooth.client.map.BluetoothMapBmessage.Status;
-import android.bluetooth.client.map.BluetoothMapBmessage.Type;
-import android.bluetooth.client.map.utils.BmsgTokenizer;
-import android.bluetooth.client.map.utils.BmsgTokenizer.Property;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.text.ParseException;
-
-class BluetoothMapBmessageParser {
-
- private final static String TAG = "BluetoothMapBmessageParser";
- private final static boolean DBG = false;
-
- private final static String CRLF = "\r\n";
-
- private final static Property BEGIN_BMSG = new Property("BEGIN", "BMSG");
- private final static Property END_BMSG = new Property("END", "BMSG");
-
- private final static Property BEGIN_VCARD = new Property("BEGIN", "VCARD");
- private final static Property END_VCARD = new Property("END", "VCARD");
-
- private final static Property BEGIN_BENV = new Property("BEGIN", "BENV");
- private final static Property END_BENV = new Property("END", "BENV");
-
- private final static Property BEGIN_BBODY = new Property("BEGIN", "BBODY");
- private final static Property END_BBODY = new Property("END", "BBODY");
-
- private final static Property BEGIN_MSG = new Property("BEGIN", "MSG");
- private final static Property END_MSG = new Property("END", "MSG");
-
- private final static int CRLF_LEN = 2;
-
- /*
- * length of "container" for 'message' in bmessage-body-content:
- * BEGIN:MSG<CRLF> + <CRLF> + END:MSG<CRFL>
- */
- private final static int MSG_CONTAINER_LEN = 22;
-
- private BmsgTokenizer mParser;
-
- private final BluetoothMapBmessage mBmsg;
-
- private BluetoothMapBmessageParser() {
- mBmsg = new BluetoothMapBmessage();
- }
-
- static public BluetoothMapBmessage createBmessage(String str) {
- BluetoothMapBmessageParser p = new BluetoothMapBmessageParser();
-
- if (DBG) {
- Log.d(TAG, "actual wired contents: " + str);
- }
-
- try {
- p.parse(str);
- } catch (IOException e) {
- Log.e(TAG, "I/O exception when parsing bMessage", e);
- return null;
- } catch (ParseException e) {
- Log.e(TAG, "Cannot parse bMessage", e);
- return null;
- }
-
- return p.mBmsg;
- }
-
- private ParseException expected(Property... props) {
- boolean first = true;
- StringBuilder sb = new StringBuilder();
-
- for (Property prop : props) {
- if (!first) {
- sb.append(" or ");
- }
- sb.append(prop);
- first = false;
- }
-
- return new ParseException("Expected: " + sb.toString(), mParser.pos());
- }
-
- private void parse(String str) throws IOException, ParseException {
-
- Property prop;
-
- /*
- * <bmessage-object>::= { "BEGIN:BMSG" <CRLF> <bmessage-property>
- * [<bmessage-originator>]* <bmessage-envelope> "END:BMSG" <CRLF> }
- */
-
- mParser = new BmsgTokenizer(str + CRLF);
-
- prop = mParser.next();
- if (!prop.equals(BEGIN_BMSG)) {
- throw expected(BEGIN_BMSG);
- }
-
- prop = parseProperties();
-
- while (prop.equals(BEGIN_VCARD)) {
-
- /* <bmessage-originator>::= <vcard> <CRLF> */
-
- StringBuilder vcard = new StringBuilder();
- prop = extractVcard(vcard);
-
- VCardEntry entry = parseVcard(vcard.toString());
- mBmsg.mOriginators.add(entry);
- }
-
- if (!prop.equals(BEGIN_BENV)) {
- throw expected(BEGIN_BENV);
- }
-
- prop = parseEnvelope(1);
-
- if (!prop.equals(END_BMSG)) {
- throw expected(END_BENV);
- }
-
- /*
- * there should be no meaningful data left in stream here so we just
- * ignore whatever is left
- */
-
- mParser = null;
- }
-
- private Property parseProperties() throws ParseException {
-
- Property prop;
-
- /*
- * <bmessage-property>::=<bmessage-version-property>
- * <bmessage-readstatus-property> <bmessage-type-property>
- * <bmessage-folder-property> <bmessage-version-property>::="VERSION:"
- * <common-digit>*"."<common-digit>* <CRLF>
- * <bmessage-readstatus-property>::="STATUS:" 'readstatus' <CRLF>
- * <bmessage-type-property>::="TYPE:" 'type' <CRLF>
- * <bmessage-folder-property>::="FOLDER:" 'foldername' <CRLF>
- */
-
- do {
- prop = mParser.next();
-
- if (prop.name.equals("VERSION")) {
- mBmsg.mBmsgVersion = prop.value;
-
- } else if (prop.name.equals("STATUS")) {
- for (Status s : Status.values()) {
- if (prop.value.equals(s.toString())) {
- mBmsg.mBmsgStatus = s;
- break;
- }
- }
-
- } else if (prop.name.equals("TYPE")) {
- for (Type t : Type.values()) {
- if (prop.value.equals(t.toString())) {
- mBmsg.mBmsgType = t;
- break;
- }
- }
-
- } else if (prop.name.equals("FOLDER")) {
- mBmsg.mBmsgFolder = prop.value;
-
- }
-
- } while (!prop.equals(BEGIN_VCARD) && !prop.equals(BEGIN_BENV));
-
- return prop;
- }
-
- private Property parseEnvelope(int level) throws IOException, ParseException {
-
- Property prop;
-
- /*
- * we can support as many nesting level as we want, but MAP spec clearly
- * defines that there should be no more than 3 levels. so we verify it
- * here.
- */
-
- if (level > 3) {
- throw new ParseException("bEnvelope is nested more than 3 times", mParser.pos());
- }
-
- /*
- * <bmessage-envelope> ::= { "BEGIN:BENV" <CRLF> [<bmessage-recipient>]*
- * <bmessage-envelope> | <bmessage-content> "END:BENV" <CRLF> }
- */
-
- prop = mParser.next();
-
- while (prop.equals(BEGIN_VCARD)) {
-
- /* <bmessage-originator>::= <vcard> <CRLF> */
-
- StringBuilder vcard = new StringBuilder();
- prop = extractVcard(vcard);
-
- if (level == 1) {
- VCardEntry entry = parseVcard(vcard.toString());
- mBmsg.mRecipients.add(entry);
- }
- }
-
- if (prop.equals(BEGIN_BENV)) {
- prop = parseEnvelope(level + 1);
-
- } else if (prop.equals(BEGIN_BBODY)) {
- prop = parseBody();
-
- } else {
- throw expected(BEGIN_BENV, BEGIN_BBODY);
- }
-
- if (!prop.equals(END_BENV)) {
- throw expected(END_BENV);
- }
-
- return mParser.next();
- }
-
- private Property parseBody() throws IOException, ParseException {
-
- Property prop;
-
- /*
- * <bmessage-content>::= { "BEGIN:BBODY"<CRLF> [<bmessage-body-part-ID>
- * <CRLF>] <bmessage-body-property> <bmessage-body-content>* <CRLF>
- * "END:BBODY"<CRLF> } <bmessage-body-part-ID>::="PARTID:" 'Part-ID'
- * <bmessage-body-property>::=[<bmessage-body-encoding-property>]
- * [<bmessage-body-charset-property>]
- * [<bmessage-body-language-property>]
- * <bmessage-body-content-length-property>
- * <bmessage-body-encoding-property>::="ENCODING:"'encoding' <CRLF>
- * <bmessage-body-charset-property>::="CHARSET:"'charset' <CRLF>
- * <bmessage-body-language-property>::="LANGUAGE:"'language' <CRLF>
- * <bmessage-body-content-length-property>::= "LENGTH:" <common-digit>*
- * <CRLF>
- */
-
- do {
- prop = mParser.next();
-
- if (prop.name.equals("PARTID")) {
- } else if (prop.name.equals("ENCODING")) {
- mBmsg.mBbodyEncoding = prop.value;
-
- } else if (prop.name.equals("CHARSET")) {
- mBmsg.mBbodyCharset = prop.value;
-
- } else if (prop.name.equals("LANGUAGE")) {
- mBmsg.mBbodyLanguage = prop.value;
-
- } else if (prop.name.equals("LENGTH")) {
- try {
- mBmsg.mBbodyLength = Integer.parseInt(prop.value);
- } catch (NumberFormatException e) {
- throw new ParseException("Invalid LENGTH value", mParser.pos());
- }
-
- }
-
- } while (!prop.equals(BEGIN_MSG));
-
- /*
- * check that the charset is always set to UTF-8. We expect only text transfer (in lieu with
- * the MAPv12 specifying only RFC2822 (text only) for MMS/EMAIL and SMS do not support
- * non-text content. If the charset is not set to UTF-8, it is safe to set the message as
- * empty. We force the getMessage (see BluetoothMasClient) to only call getMessage with
- * UTF-8 as the MCE is not obliged to support native charset.
- */
- if (!mBmsg.mBbodyCharset.equals("UTF-8")) {
- Log.e(TAG, "The charset was not set to charset UTF-8: " + mBmsg.mBbodyCharset);
- }
-
- /*
- * <bmessage-body-content>::={ "BEGIN:MSG"<CRLF> 'message'<CRLF>
- * "END:MSG"<CRLF> }
- */
-
- int messageLen = mBmsg.mBbodyLength - MSG_CONTAINER_LEN;
- int offset = messageLen + CRLF_LEN;
- int restartPos = mParser.pos() + offset;
-
- /*
- * length is specified in bytes so we need to convert from unicode
- * string back to bytes array
- */
-
- String remng = mParser.remaining();
- byte[] data = remng.getBytes();
-
- /* restart parsing from after 'message'<CRLF> */
- mParser = new BmsgTokenizer(new String(data, offset, data.length - offset), restartPos);
-
- prop = mParser.next(true);
-
- if (prop != null) {
- if (prop.equals(END_MSG)) {
- if (mBmsg.mBbodyCharset.equals("UTF-8")) {
- mBmsg.mMessage = new String(data, 0, messageLen, StandardCharsets.UTF_8);
- } else {
- mBmsg.mMessage = null;
- }
- } else {
- /* Handle possible exception for incorrect LENGTH value
- * from MSE while parsing GET Message response */
- Log.e(TAG, "Prop Invalid: "+ prop.toString());
- Log.e(TAG, "Possible Invalid LENGTH value");
- throw expected(END_MSG);
- }
- } else {
-
- data = null;
-
- /*
- * now we check if bMessage can be parsed if LENGTH is handled as
- * number of characters instead of number of bytes
- */
- if (offset < 0 || offset > remng.length()) {
- /* Handle possible exception for incorrect LENGTH value
- * from MSE while parsing GET Message response */
- throw new ParseException("Invalid LENGTH value", mParser.pos());
- }
-
- Log.w(TAG, "byte LENGTH seems to be invalid, trying with char length");
-
- mParser = new BmsgTokenizer(remng.substring(offset));
-
- prop = mParser.next();
-
- if (!prop.equals(END_MSG)) {
- throw expected(END_MSG);
- }
-
- if (mBmsg.mBbodyCharset.equals("UTF-8")) {
- mBmsg.mMessage = remng.substring(0, messageLen);
- } else {
- mBmsg.mMessage = null;
- }
- }
-
- prop = mParser.next();
-
- if (!prop.equals(END_BBODY)) {
- throw expected(END_BBODY);
- }
-
- return mParser.next();
- }
-
- private Property extractVcard(StringBuilder out) throws IOException, ParseException {
- Property prop;
-
- out.append(BEGIN_VCARD).append(CRLF);
-
- do {
- prop = mParser.next();
- out.append(prop).append(CRLF);
- } while (!prop.equals(END_VCARD));
-
- return mParser.next();
- }
-
- private class VcardHandler implements VCardEntryHandler {
-
- VCardEntry vcard;
-
- @Override
- public void onStart() {
- }
-
- @Override
- public void onEntryCreated(VCardEntry entry) {
- vcard = entry;
- }
-
- @Override
- public void onEnd() {
- }
- };
-
- private VCardEntry parseVcard(String str) throws IOException, ParseException {
- VCardEntry vcard = null;
-
- try {
- VCardParser p = new VCardParser_V21();
- VCardEntryConstructor c = new VCardEntryConstructor();
- VcardHandler handler = new VcardHandler();
- c.addEntryHandler(handler);
- p.addInterpreter(c);
- p.parse(new ByteArrayInputStream(str.getBytes()));
-
- vcard = handler.vcard;
-
- } catch (VCardVersionException e1) {
-
- try {
- VCardParser p = new VCardParser_V30();
- VCardEntryConstructor c = new VCardEntryConstructor();
- VcardHandler handler = new VcardHandler();
- c.addEntryHandler(handler);
- p.addInterpreter(c);
- p.parse(new ByteArrayInputStream(str.getBytes()));
-
- vcard = handler.vcard;
-
- } catch (VCardVersionException e2) {
- // will throw below
- } catch (VCardException e2) {
- // will throw below
- }
-
- } catch (VCardException e1) {
- // will throw below
- }
-
- if (vcard == null) {
- throw new ParseException("Cannot parse vCard object (neither 2.1 nor 3.0?)",
- mParser.pos());
- }
-
- return vcard;
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMapEventReport.java b/android/bluetooth/client/map/BluetoothMapEventReport.java
deleted file mode 100644
index 5963db4..0000000
--- a/android/bluetooth/client/map/BluetoothMapEventReport.java
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-import android.util.Log;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlPullParserFactory;
-
-import java.io.DataInputStream;
-import java.io.IOException;
-import java.math.BigInteger;
-import java.util.HashMap;
-
-/**
- * Object representation of event report received by MNS
- * <p>
- * This object will be received in {@link BluetoothMasClient#EVENT_EVENT_REPORT}
- * callback message.
- */
-public class BluetoothMapEventReport {
-
- private final static String TAG = "BluetoothMapEventReport";
-
- public enum Type {
- NEW_MESSAGE("NewMessage"), DELIVERY_SUCCESS("DeliverySuccess"),
- SENDING_SUCCESS("SendingSuccess"), DELIVERY_FAILURE("DeliveryFailure"),
- SENDING_FAILURE("SendingFailure"), MEMORY_FULL("MemoryFull"),
- MEMORY_AVAILABLE("MemoryAvailable"), MESSAGE_DELETED("MessageDeleted"),
- MESSAGE_SHIFT("MessageShift");
-
- private final String mSpecName;
-
- private Type(String specName) {
- mSpecName = specName;
- }
-
- @Override
- public String toString() {
- return mSpecName;
- }
- }
-
- private final Type mType;
-
- private final String mHandle;
-
- private final String mFolder;
-
- private final String mOldFolder;
-
- private final BluetoothMapBmessage.Type mMsgType;
-
- private BluetoothMapEventReport(HashMap<String, String> attrs) throws IllegalArgumentException {
- mType = parseType(attrs.get("type"));
-
- if (mType != Type.MEMORY_FULL && mType != Type.MEMORY_AVAILABLE) {
- String handle = attrs.get("handle");
- try {
- /* just to validate */
- new BigInteger(attrs.get("handle"), 16);
-
- mHandle = attrs.get("handle");
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException("Invalid value for handle:" + handle);
- }
- } else {
- mHandle = null;
- }
-
- mFolder = attrs.get("folder");
-
- mOldFolder = attrs.get("old_folder");
-
- if (mType != Type.MEMORY_FULL && mType != Type.MEMORY_AVAILABLE) {
- String s = attrs.get("msg_type");
-
- if ("".equals(s)) {
- // Some phones (e.g. SGS3 for MessageDeleted) send empty
- // msg_type, in such case leave it as null rather than throw
- // parse exception
- mMsgType = null;
- } else {
- mMsgType = parseMsgType(s);
- }
- } else {
- mMsgType = null;
- }
- }
-
- private Type parseType(String type) throws IllegalArgumentException {
- for (Type t : Type.values()) {
- if (t.toString().equals(type)) {
- return t;
- }
- }
-
- throw new IllegalArgumentException("Invalid value for type: " + type);
- }
-
- private BluetoothMapBmessage.Type parseMsgType(String msgType) throws IllegalArgumentException {
- for (BluetoothMapBmessage.Type t : BluetoothMapBmessage.Type.values()) {
- if (t.name().equals(msgType)) {
- return t;
- }
- }
-
- throw new IllegalArgumentException("Invalid value for msg_type: " + msgType);
- }
-
- /**
- * @return {@link BluetoothMapEventReport.Type} object corresponding to
- * <code>type</code> application parameter in MAP specification
- */
- public Type getType() {
- return mType;
- }
-
- /**
- * @return value corresponding to <code>handle</code> parameter in MAP
- * specification
- */
- public String getHandle() {
- return mHandle;
- }
-
- /**
- * @return value corresponding to <code>folder</code> parameter in MAP
- * specification
- */
- public String getFolder() {
- return mFolder;
- }
-
- /**
- * @return value corresponding to <code>old_folder</code> parameter in MAP
- * specification
- */
- public String getOldFolder() {
- return mOldFolder;
- }
-
- /**
- * @return {@link BluetoothMapBmessage.Type} object corresponding to
- * <code>msg_type</code> application parameter in MAP specification
- */
- public BluetoothMapBmessage.Type getMsgType() {
- return mMsgType;
- }
-
- @Override
- public String toString() {
- JSONObject json = new JSONObject();
-
- try {
- json.put("type", mType);
- json.put("handle", mHandle);
- json.put("folder", mFolder);
- json.put("old_folder", mOldFolder);
- json.put("msg_type", mMsgType);
- } catch (JSONException e) {
- // do nothing
- }
-
- return json.toString();
- }
-
- static BluetoothMapEventReport fromStream(DataInputStream in) {
- BluetoothMapEventReport ev = null;
-
- try {
- XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();
- xpp.setInput(in, "utf-8");
-
- int event = xpp.getEventType();
- while (event != XmlPullParser.END_DOCUMENT) {
- switch (event) {
- case XmlPullParser.START_TAG:
- if (xpp.getName().equals("event")) {
- HashMap<String, String> attrs = new HashMap<String, String>();
-
- for (int i = 0; i < xpp.getAttributeCount(); i++) {
- attrs.put(xpp.getAttributeName(i), xpp.getAttributeValue(i));
- }
-
- ev = new BluetoothMapEventReport(attrs);
-
- // return immediately, only one event should be here
- return ev;
- }
- break;
- }
-
- event = xpp.next();
- }
-
- } catch (XmlPullParserException e) {
- Log.e(TAG, "XML parser error when parsing XML", e);
- } catch (IOException e) {
- Log.e(TAG, "I/O error when parsing XML", e);
- } catch (IllegalArgumentException e) {
- Log.e(TAG, "Invalid event received", e);
- }
-
- return ev;
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMapFolderListing.java b/android/bluetooth/client/map/BluetoothMapFolderListing.java
deleted file mode 100644
index f0494b3..0000000
--- a/android/bluetooth/client/map/BluetoothMapFolderListing.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-import android.util.Log;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlPullParserFactory;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-
-class BluetoothMapFolderListing {
-
- private static final String TAG = "BluetoothMasFolderListing";
-
- private final ArrayList<String> mFolders;
-
- public BluetoothMapFolderListing(InputStream in) {
- mFolders = new ArrayList<String>();
-
- parse(in);
- }
-
- public void parse(InputStream in) {
-
- try {
- XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();
- xpp.setInput(in, "utf-8");
-
- int event = xpp.getEventType();
- while (event != XmlPullParser.END_DOCUMENT) {
- switch (event) {
- case XmlPullParser.START_TAG:
- if (xpp.getName().equals("folder")) {
- mFolders.add(xpp.getAttributeValue(null, "name"));
- }
- break;
- }
-
- event = xpp.next();
- }
-
- } catch (XmlPullParserException e) {
- Log.e(TAG, "XML parser error when parsing XML", e);
- } catch (IOException e) {
- Log.e(TAG, "I/O error when parsing XML", e);
- }
- }
-
- public ArrayList<String> getList() {
- return mFolders;
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMapMessage.java b/android/bluetooth/client/map/BluetoothMapMessage.java
deleted file mode 100644
index 5ce6c4b..0000000
--- a/android/bluetooth/client/map/BluetoothMapMessage.java
+++ /dev/null
@@ -1,338 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-import android.bluetooth.client.map.utils.ObexTime;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.math.BigInteger;
-import java.util.Date;
-import java.util.HashMap;
-
-/**
- * Object representation of message received in messages listing
- * <p>
- * This object will be received in
- * {@link BluetoothMasClient#EVENT_GET_MESSAGES_LISTING} callback message.
- */
-public class BluetoothMapMessage {
-
- private final String mHandle;
-
- private final String mSubject;
-
- private final Date mDateTime;
-
- private final String mSenderName;
-
- private final String mSenderAddressing;
-
- private final String mReplytoAddressing;
-
- private final String mRecipientName;
-
- private final String mRecipientAddressing;
-
- private final Type mType;
-
- private final int mSize;
-
- private final boolean mText;
-
- private final ReceptionStatus mReceptionStatus;
-
- private final int mAttachmentSize;
-
- private final boolean mPriority;
-
- private final boolean mRead;
-
- private final boolean mSent;
-
- private final boolean mProtected;
-
- public enum Type {
- UNKNOWN, EMAIL, SMS_GSM, SMS_CDMA, MMS
- };
-
- public enum ReceptionStatus {
- UNKNOWN, COMPLETE, FRACTIONED, NOTIFICATION
- }
-
- BluetoothMapMessage(HashMap<String, String> attrs) throws IllegalArgumentException {
- int size;
-
- try {
- /* just to validate */
- new BigInteger(attrs.get("handle"), 16);
-
- mHandle = attrs.get("handle");
- } catch (NumberFormatException e) {
- /*
- * handle MUST have proper value, if it does not then throw
- * something here
- */
- throw new IllegalArgumentException(e);
- }
-
- mSubject = attrs.get("subject");
- String dateTime = attrs.get("datetime");
- //Handle possible NPE when not able to retreive datetime attribute
- if(dateTime != null){
- mDateTime = (new ObexTime(dateTime)).getTime();
- } else {
- mDateTime = null;
- }
-
-
- mSenderName = attrs.get("sender_name");
-
- mSenderAddressing = attrs.get("sender_addressing");
-
- mReplytoAddressing = attrs.get("replyto_addressing");
-
- mRecipientName = attrs.get("recipient_name");
-
- mRecipientAddressing = attrs.get("recipient_addressing");
-
- mType = strToType(attrs.get("type"));
-
- try {
- size = Integer.parseInt(attrs.get("size"));
- } catch (NumberFormatException e) {
- size = 0;
- }
-
- mSize = size;
-
- mText = yesnoToBoolean(attrs.get("text"));
-
- mReceptionStatus = strToReceptionStatus(attrs.get("reception_status"));
-
- try {
- size = Integer.parseInt(attrs.get("attachment_size"));
- } catch (NumberFormatException e) {
- size = 0;
- }
-
- mAttachmentSize = size;
-
- mPriority = yesnoToBoolean(attrs.get("priority"));
-
- mRead = yesnoToBoolean(attrs.get("read"));
-
- mSent = yesnoToBoolean(attrs.get("sent"));
-
- mProtected = yesnoToBoolean(attrs.get("protected"));
- }
-
- private boolean yesnoToBoolean(String yesno) {
- return "yes".equals(yesno);
- }
-
- private Type strToType(String s) {
- if ("EMAIL".equals(s)) {
- return Type.EMAIL;
- } else if ("SMS_GSM".equals(s)) {
- return Type.SMS_GSM;
- } else if ("SMS_CDMA".equals(s)) {
- return Type.SMS_CDMA;
- } else if ("MMS".equals(s)) {
- return Type.MMS;
- }
-
- return Type.UNKNOWN;
- }
-
- private ReceptionStatus strToReceptionStatus(String s) {
- if ("complete".equals(s)) {
- return ReceptionStatus.COMPLETE;
- } else if ("fractioned".equals(s)) {
- return ReceptionStatus.FRACTIONED;
- } else if ("notification".equals(s)) {
- return ReceptionStatus.NOTIFICATION;
- }
-
- return ReceptionStatus.UNKNOWN;
- }
-
- @Override
- public String toString() {
- JSONObject json = new JSONObject();
-
- try {
- json.put("handle", mHandle);
- json.put("subject", mSubject);
- json.put("datetime", mDateTime);
- json.put("sender_name", mSenderName);
- json.put("sender_addressing", mSenderAddressing);
- json.put("replyto_addressing", mReplytoAddressing);
- json.put("recipient_name", mRecipientName);
- json.put("recipient_addressing", mRecipientAddressing);
- json.put("type", mType);
- json.put("size", mSize);
- json.put("text", mText);
- json.put("reception_status", mReceptionStatus);
- json.put("attachment_size", mAttachmentSize);
- json.put("priority", mPriority);
- json.put("read", mRead);
- json.put("sent", mSent);
- json.put("protected", mProtected);
- } catch (JSONException e) {
- // do nothing
- }
-
- return json.toString();
- }
-
- /**
- * @return value corresponding to <code>handle</code> parameter in MAP
- * specification
- */
- public String getHandle() {
- return mHandle;
- }
-
- /**
- * @return value corresponding to <code>subject</code> parameter in MAP
- * specification
- */
- public String getSubject() {
- return mSubject;
- }
-
- /**
- * @return <code>Date</code> object corresponding to <code>datetime</code>
- * parameter in MAP specification
- */
- public Date getDateTime() {
- return mDateTime;
- }
-
- /**
- * @return value corresponding to <code>sender_name</code> parameter in MAP
- * specification
- */
- public String getSenderName() {
- return mSenderName;
- }
-
- /**
- * @return value corresponding to <code>sender_addressing</code> parameter
- * in MAP specification
- */
- public String getSenderAddressing() {
- return mSenderAddressing;
- }
-
- /**
- * @return value corresponding to <code>replyto_addressing</code> parameter
- * in MAP specification
- */
- public String getReplytoAddressing() {
- return mReplytoAddressing;
- }
-
- /**
- * @return value corresponding to <code>recipient_name</code> parameter in
- * MAP specification
- */
- public String getRecipientName() {
- return mRecipientName;
- }
-
- /**
- * @return value corresponding to <code>recipient_addressing</code>
- * parameter in MAP specification
- */
- public String getRecipientAddressing() {
- return mRecipientAddressing;
- }
-
- /**
- * @return {@link Type} object corresponding to <code>type</code> parameter
- * in MAP specification
- */
- public Type getType() {
- return mType;
- }
-
- /**
- * @return value corresponding to <code>size</code> parameter in MAP
- * specification
- */
- public int getSize() {
- return mSize;
- }
-
- /**
- * @return {@link .ReceptionStatus} object corresponding to
- * <code>reception_status</code> parameter in MAP specification
- */
- public ReceptionStatus getReceptionStatus() {
- return mReceptionStatus;
- }
-
- /**
- * @return value corresponding to <code>attachment_size</code> parameter in
- * MAP specification
- */
- public int getAttachmentSize() {
- return mAttachmentSize;
- }
-
- /**
- * @return value corresponding to <code>text</code> parameter in MAP
- * specification
- */
- public boolean isText() {
- return mText;
- }
-
- /**
- * @return value corresponding to <code>priority</code> parameter in MAP
- * specification
- */
- public boolean isPriority() {
- return mPriority;
- }
-
- /**
- * @return value corresponding to <code>read</code> parameter in MAP
- * specification
- */
- public boolean isRead() {
- return mRead;
- }
-
- /**
- * @return value corresponding to <code>sent</code> parameter in MAP
- * specification
- */
- public boolean isSent() {
- return mSent;
- }
-
- /**
- * @return value corresponding to <code>protected</code> parameter in MAP
- * specification
- */
- public boolean isProtected() {
- return mProtected;
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMapMessagesListing.java b/android/bluetooth/client/map/BluetoothMapMessagesListing.java
deleted file mode 100644
index 2fb3dea..0000000
--- a/android/bluetooth/client/map/BluetoothMapMessagesListing.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import android.util.Log;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlPullParserFactory;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.HashMap;
-
-class BluetoothMapMessagesListing {
-
- private static final String TAG = "BluetoothMapMessagesListing";
-
- private final ArrayList<BluetoothMapMessage> mMessages;
-
- public BluetoothMapMessagesListing(InputStream in) {
- mMessages = new ArrayList<BluetoothMapMessage>();
-
- parse(in);
- }
-
- public void parse(InputStream in) {
-
- try {
- XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();
- xpp.setInput(in, "utf-8");
-
- int event = xpp.getEventType();
- while (event != XmlPullParser.END_DOCUMENT) {
- switch (event) {
- case XmlPullParser.START_TAG:
- if (xpp.getName().equals("msg")) {
-
- HashMap<String, String> attrs = new HashMap<String, String>();
-
- for (int i = 0; i < xpp.getAttributeCount(); i++) {
- attrs.put(xpp.getAttributeName(i), xpp.getAttributeValue(i));
- }
-
- try {
- BluetoothMapMessage msg = new BluetoothMapMessage(attrs);
- mMessages.add(msg);
- } catch (IllegalArgumentException e) {
- /* TODO: provide something more useful here */
- Log.w(TAG, "Invalid <msg/>");
- }
- }
- break;
- }
-
- event = xpp.next();
- }
-
- } catch (XmlPullParserException e) {
- Log.e(TAG, "XML parser error when parsing XML", e);
- } catch (IOException e) {
- Log.e(TAG, "I/O error when parsing XML", e);
- }
- }
-
- public ArrayList<BluetoothMapMessage> getList() {
- return mMessages;
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMapRfcommTransport.java b/android/bluetooth/client/map/BluetoothMapRfcommTransport.java
deleted file mode 100644
index 5bec982..0000000
--- a/android/bluetooth/client/map/BluetoothMapRfcommTransport.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import android.bluetooth.BluetoothSocket;
-
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-import javax.obex.ObexTransport;
-
-class BluetoothMapRfcommTransport implements ObexTransport {
- private final BluetoothSocket mSocket;
-
- public BluetoothMapRfcommTransport(BluetoothSocket socket) {
- super();
- mSocket = socket;
- }
-
- @Override
- public void create() throws IOException {
- }
-
- @Override
- public void listen() throws IOException {
- }
-
- @Override
- public void close() throws IOException {
- mSocket.close();
- }
-
- @Override
- public void connect() throws IOException {
- }
-
- @Override
- public void disconnect() throws IOException {
- }
-
- @Override
- public InputStream openInputStream() throws IOException {
- return mSocket.getInputStream();
- }
-
- @Override
- public OutputStream openOutputStream() throws IOException {
- return mSocket.getOutputStream();
- }
-
- @Override
- public DataInputStream openDataInputStream() throws IOException {
- return new DataInputStream(openInputStream());
- }
-
- @Override
- public DataOutputStream openDataOutputStream() throws IOException {
- return new DataOutputStream(openOutputStream());
- }
-
- @Override
- public int getMaxTransmitPacketSize() {
- return -1;
- }
-
- @Override
- public int getMaxReceivePacketSize() {
- return -1;
- }
-
- @Override
- public boolean isSrmSupported() {
- return false;
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasClient.java b/android/bluetooth/client/map/BluetoothMasClient.java
deleted file mode 100644
index 87f5a38..0000000
--- a/android/bluetooth/client/map/BluetoothMasClient.java
+++ /dev/null
@@ -1,1106 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothMasInstance;
-import android.bluetooth.BluetoothSocket;
-import android.bluetooth.SdpMasRecord;
-import android.os.Handler;
-import android.os.Message;
-import android.util.Log;
-
-import android.bluetooth.client.map.BluetoothMasRequestSetMessageStatus.StatusIndicator;
-import android.bluetooth.client.map.utils.ObexTime;
-
-import java.io.IOException;
-import java.lang.ref.WeakReference;
-import java.math.BigInteger;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.Iterator;
-
-import javax.obex.ObexTransport;
-
-public class BluetoothMasClient {
-
- private final static String TAG = "BluetoothMasClient";
-
- private static final int SOCKET_CONNECTED = 10;
-
- private static final int SOCKET_ERROR = 11;
-
- /**
- * Callback message sent when connection state changes
- * <p>
- * <code>arg1</code> is set to {@link #STATUS_OK} when connection is
- * established successfully and {@link #STATUS_FAILED} when connection
- * either failed or was disconnected (depends on request from application)
- *
- * @see #connect()
- * @see #disconnect()
- */
- public static final int EVENT_CONNECT = 1;
-
- /**
- * Callback message sent when MSE accepted update inbox request
- *
- * @see #updateInbox()
- */
- public static final int EVENT_UPDATE_INBOX = 2;
-
- /**
- * Callback message sent when path is changed
- * <p>
- * <code>obj</code> is set to path currently set on MSE
- *
- * @see #setFolderRoot()
- * @see #setFolderUp()
- * @see #setFolderDown(String)
- */
- public static final int EVENT_SET_PATH = 3;
-
- /**
- * Callback message sent when folder listing is received
- * <p>
- * <code>obj</code> contains ArrayList of sub-folder names
- *
- * @see #getFolderListing()
- * @see #getFolderListing(int, int)
- */
- public static final int EVENT_GET_FOLDER_LISTING = 4;
-
- /**
- * Callback message sent when folder listing size is received
- * <p>
- * <code>obj</code> contains number of items in folder listing
- *
- * @see #getFolderListingSize()
- */
- public static final int EVENT_GET_FOLDER_LISTING_SIZE = 5;
-
- /**
- * Callback message sent when messages listing is received
- * <p>
- * <code>obj</code> contains ArrayList of {@link BluetoothMapBmessage}
- *
- * @see #getMessagesListing(String, int)
- * @see #getMessagesListing(String, int, MessagesFilter, int)
- * @see #getMessagesListing(String, int, MessagesFilter, int, int, int)
- */
- public static final int EVENT_GET_MESSAGES_LISTING = 6;
-
- /**
- * Callback message sent when message is received
- * <p>
- * <code>obj</code> contains {@link BluetoothMapBmessage}
- *
- * @see #getMessage(String, CharsetType, boolean)
- */
- public static final int EVENT_GET_MESSAGE = 7;
-
- /**
- * Callback message sent when message status is changed
- *
- * @see #setMessageDeletedStatus(String, boolean)
- * @see #setMessageReadStatus(String, boolean)
- */
- public static final int EVENT_SET_MESSAGE_STATUS = 8;
-
- /**
- * Callback message sent when message is pushed to MSE
- * <p>
- * <code>obj</code> contains handle of message as allocated by MSE
- *
- * @see #pushMessage(String, BluetoothMapBmessage, CharsetType)
- * @see #pushMessage(String, BluetoothMapBmessage, CharsetType, boolean,
- * boolean)
- */
- public static final int EVENT_PUSH_MESSAGE = 9;
-
- /**
- * Callback message sent when notification status is changed
- * <p>
- * <code>obj</code> contains <code>1</code> if notifications are enabled and
- * <code>0</code> otherwise
- *
- * @see #setNotificationRegistration(boolean)
- */
- public static final int EVENT_SET_NOTIFICATION_REGISTRATION = 10;
-
- /**
- * Callback message sent when event report is received from MSE to MNS
- * <p>
- * <code>obj</code> contains {@link BluetoothMapEventReport}
- *
- * @see #setNotificationRegistration(boolean)
- */
- public static final int EVENT_EVENT_REPORT = 11;
-
- /**
- * Callback message sent when messages listing size is received
- * <p>
- * <code>obj</code> contains number of items in messages listing
- *
- * @see #getMessagesListingSize()
- */
- public static final int EVENT_GET_MESSAGES_LISTING_SIZE = 12;
-
- /**
- * Status for callback message when request is successful
- */
- public static final int STATUS_OK = 0;
-
- /**
- * Status for callback message when request is not successful
- */
- public static final int STATUS_FAILED = 1;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_DEFAULT = 0x00000000;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_SUBJECT = 0x00000001;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_DATETIME = 0x00000002;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_SENDER_NAME = 0x00000004;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_SENDER_ADDRESSING = 0x00000008;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_RECIPIENT_NAME = 0x00000010;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_RECIPIENT_ADDRESSING = 0x00000020;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_TYPE = 0x00000040;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_SIZE = 0x00000080;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_RECEPTION_STATUS = 0x00000100;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_TEXT = 0x00000200;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_ATTACHMENT_SIZE = 0x00000400;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_PRIORITY = 0x00000800;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_READ = 0x00001000;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_SENT = 0x00002000;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_PROTECTED = 0x00004000;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_REPLYTO_ADDRESSING = 0x00008000;
-
- public enum ConnectionState {
- DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING;
- }
-
- public enum CharsetType {
- NATIVE, UTF_8;
- }
-
- /** device associated with client */
- private final BluetoothDevice mDevice;
-
- /** MAS instance associated with client */
- private final SdpMasRecord mMas;
-
- /** callback handler to application */
- private final Handler mCallback;
-
- private ConnectionState mConnectionState = ConnectionState.DISCONNECTED;
-
- private boolean mNotificationEnabled = false;
-
- private SocketConnectThread mConnectThread = null;
-
- private ObexTransport mObexTransport = null;
-
- private BluetoothMasObexClientSession mObexSession = null;
-
- private SessionHandler mSessionHandler = null;
-
- private BluetoothMnsService mMnsService = null;
-
- private ArrayDeque<String> mPath = null;
-
- private static class SessionHandler extends Handler {
-
- private final WeakReference<BluetoothMasClient> mClient;
-
- public SessionHandler(BluetoothMasClient client) {
- super();
-
- mClient = new WeakReference<BluetoothMasClient>(client);
- }
-
- @Override
- public void handleMessage(Message msg) {
-
- BluetoothMasClient client = mClient.get();
- if (client == null) {
- return;
- }
- Log.v(TAG, "handleMessage "+msg.what);
-
- switch (msg.what) {
- case SOCKET_ERROR:
- client.mConnectThread = null;
- client.sendToClient(EVENT_CONNECT, false);
- break;
-
- case SOCKET_CONNECTED:
- client.mConnectThread = null;
-
- client.mObexTransport = (ObexTransport) msg.obj;
-
- client.mObexSession = new BluetoothMasObexClientSession(client.mObexTransport,
- client.mSessionHandler);
- client.mObexSession.start();
- break;
-
- case BluetoothMasObexClientSession.MSG_OBEX_CONNECTED:
- client.mPath.clear(); // we're in root after connected
- client.mConnectionState = ConnectionState.CONNECTED;
- client.sendToClient(EVENT_CONNECT, true);
- break;
-
- case BluetoothMasObexClientSession.MSG_OBEX_DISCONNECTED:
- client.mConnectionState = ConnectionState.DISCONNECTED;
- client.mNotificationEnabled = false;
- client.mObexSession = null;
- client.sendToClient(EVENT_CONNECT, false);
- break;
-
- case BluetoothMasObexClientSession.MSG_REQUEST_COMPLETED:
- BluetoothMasRequest request = (BluetoothMasRequest) msg.obj;
- int status = request.isSuccess() ? STATUS_OK : STATUS_FAILED;
-
- Log.v(TAG, "MSG_REQUEST_COMPLETED (" + status + ") for "
- + request.getClass().getName());
-
- if (request instanceof BluetoothMasRequestUpdateInbox) {
- client.sendToClient(EVENT_UPDATE_INBOX, request.isSuccess());
-
- } else if (request instanceof BluetoothMasRequestSetPath) {
- if (request.isSuccess()) {
- BluetoothMasRequestSetPath req = (BluetoothMasRequestSetPath) request;
- switch (req.mDir) {
- case UP:
- if (client.mPath.size() > 0) {
- client.mPath.removeLast();
- }
- break;
-
- case ROOT:
- client.mPath.clear();
- break;
-
- case DOWN:
- client.mPath.addLast(req.mName);
- break;
- }
- }
-
- client.sendToClient(EVENT_SET_PATH, request.isSuccess(),
- client.getCurrentPath());
-
- } else if (request instanceof BluetoothMasRequestGetFolderListing) {
- BluetoothMasRequestGetFolderListing req = (BluetoothMasRequestGetFolderListing) request;
- ArrayList<String> folders = req.getList();
-
- client.sendToClient(EVENT_GET_FOLDER_LISTING, request.isSuccess(), folders);
-
- } else if (request instanceof BluetoothMasRequestGetFolderListingSize) {
- int size = ((BluetoothMasRequestGetFolderListingSize) request).getSize();
-
- client.sendToClient(EVENT_GET_FOLDER_LISTING_SIZE, request.isSuccess(),
- size);
-
- } else if (request instanceof BluetoothMasRequestGetMessagesListing) {
- BluetoothMasRequestGetMessagesListing req = (BluetoothMasRequestGetMessagesListing) request;
- ArrayList<BluetoothMapMessage> msgs = req.getList();
-
- client.sendToClient(EVENT_GET_MESSAGES_LISTING, request.isSuccess(), msgs);
-
- } else if (request instanceof BluetoothMasRequestGetMessage) {
- BluetoothMasRequestGetMessage req = (BluetoothMasRequestGetMessage) request;
- BluetoothMapBmessage bmsg = req.getMessage();
-
- client.sendToClient(EVENT_GET_MESSAGE, request.isSuccess(), bmsg);
-
- } else if (request instanceof BluetoothMasRequestSetMessageStatus) {
- client.sendToClient(EVENT_SET_MESSAGE_STATUS, request.isSuccess());
-
- } else if (request instanceof BluetoothMasRequestPushMessage) {
- BluetoothMasRequestPushMessage req = (BluetoothMasRequestPushMessage) request;
- String handle = req.getMsgHandle();
-
- client.sendToClient(EVENT_PUSH_MESSAGE, request.isSuccess(), handle);
-
- } else if (request instanceof BluetoothMasRequestSetNotificationRegistration) {
- BluetoothMasRequestSetNotificationRegistration req = (BluetoothMasRequestSetNotificationRegistration) request;
-
- client.mNotificationEnabled = req.isSuccess() ? req.getStatus()
- : client.mNotificationEnabled;
-
- client.sendToClient(EVENT_SET_NOTIFICATION_REGISTRATION,
- request.isSuccess(),
- client.mNotificationEnabled ? 1 : 0);
- } else if (request instanceof BluetoothMasRequestGetMessagesListingSize) {
- int size = ((BluetoothMasRequestGetMessagesListingSize) request).getSize();
- client.sendToClient(EVENT_GET_MESSAGES_LISTING_SIZE, request.isSuccess(),
- size);
- }
- break;
-
- case BluetoothMnsService.EVENT_REPORT:
- /* pass event report directly to app */
- client.sendToClient(EVENT_EVENT_REPORT, true, msg.obj);
- break;
- }
- }
- }
-
- private void sendToClient(int event, boolean success) {
- sendToClient(event, success, null);
- }
-
- private void sendToClient(int event, boolean success, int param) {
- sendToClient(event, success, Integer.valueOf(param));
- }
-
- private void sendToClient(int event, boolean success, Object param) {
- // Send event, status and notification state for both sucess and failure case.
- mCallback.obtainMessage(event, success ? STATUS_OK : STATUS_FAILED, mMas.getMasInstanceId(),
- param).sendToTarget();
- }
-
- private class SocketConnectThread extends Thread {
- private BluetoothSocket socket = null;
-
- public SocketConnectThread() {
- super("SocketConnectThread");
- }
-
- @Override
- public void run() {
- try {
- socket = mDevice.createRfcommSocket(mMas.getRfcommCannelNumber());
- socket.connect();
-
- BluetoothMapRfcommTransport transport;
- transport = new BluetoothMapRfcommTransport(socket);
-
- mSessionHandler.obtainMessage(SOCKET_CONNECTED, transport).sendToTarget();
- } catch (IOException e) {
- Log.e(TAG, "Error when creating/connecting socket", e);
-
- closeSocket();
- mSessionHandler.obtainMessage(SOCKET_ERROR).sendToTarget();
- }
- }
-
- @Override
- public void interrupt() {
- closeSocket();
- }
-
- private void closeSocket() {
- try {
- if (socket != null) {
- socket.close();
- }
- } catch (IOException e) {
- Log.e(TAG, "Error when closing socket", e);
- }
- }
- }
-
- /**
- * Object representation of filters to be applied on message listing
- *
- * @see #getMessagesListing(String, int, MessagesFilter, int)
- * @see #getMessagesListing(String, int, MessagesFilter, int, int, int)
- */
- public static final class MessagesFilter {
-
- public final static byte MESSAGE_TYPE_ALL = 0x00;
- public final static byte MESSAGE_TYPE_SMS_GSM = 0x01;
- public final static byte MESSAGE_TYPE_SMS_CDMA = 0x02;
- public final static byte MESSAGE_TYPE_EMAIL = 0x04;
- public final static byte MESSAGE_TYPE_MMS = 0x08;
-
- public final static byte READ_STATUS_ANY = 0x00;
- public final static byte READ_STATUS_UNREAD = 0x01;
- public final static byte READ_STATUS_READ = 0x02;
-
- public final static byte PRIORITY_ANY = 0x00;
- public final static byte PRIORITY_HIGH = 0x01;
- public final static byte PRIORITY_NON_HIGH = 0x02;
-
- byte messageType = MESSAGE_TYPE_ALL;
-
- String periodBegin = null;
-
- String periodEnd = null;
-
- byte readStatus = READ_STATUS_ANY;
-
- String recipient = null;
-
- String originator = null;
-
- byte priority = PRIORITY_ANY;
-
- public MessagesFilter() {
- }
-
- public void setMessageType(byte filter) {
- messageType = filter;
- }
-
- public void setPeriod(Date filterBegin, Date filterEnd) {
- //Handle possible NPE for obexTime constructor utility
- if(filterBegin != null )
- periodBegin = (new ObexTime(filterBegin)).toString();
- if(filterEnd != null)
- periodEnd = (new ObexTime(filterEnd)).toString();
- }
-
- public void setReadStatus(byte readfilter) {
- readStatus = readfilter;
- }
-
- public void setRecipient(String filter) {
- if ("".equals(filter)) {
- recipient = null;
- } else {
- recipient = filter;
- }
- }
-
- public void setOriginator(String filter) {
- if ("".equals(filter)) {
- originator = null;
- } else {
- originator = filter;
- }
- }
-
- public void setPriority(byte filter) {
- priority = filter;
- }
- }
-
- /**
- * Constructs client object to communicate with single MAS instance on MSE
- *
- * @param device {@link BluetoothDevice} corresponding to remote device
- * acting as MSE
- * @param mas {@link BluetoothMasInstance} object describing MAS instance on
- * remote device
- * @param callback {@link Handler} object to which callback messages will be
- * sent Each message will have <code>arg1</code> set to either
- * {@link #STATUS_OK} or {@link #STATUS_FAILED} and
- * <code>arg2</code> to MAS instance ID. <code>obj</code> in
- * message is event specific.
- */
- public BluetoothMasClient(BluetoothDevice device, SdpMasRecord mas,
- Handler callback) {
- mDevice = device;
- mMas = mas;
- mCallback = callback;
-
- mPath = new ArrayDeque<String>();
- }
-
- /**
- * Retrieves MAS instance data associated with client
- *
- * @return instance data object
- */
- public SdpMasRecord getInstanceData() {
- return mMas;
- }
-
- /**
- * Connects to MAS instance
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_CONNECT}
- */
- public void connect() {
- if (mSessionHandler == null) {
- mSessionHandler = new SessionHandler(this);
- }
-
- if (mConnectThread == null && mObexSession == null) {
- mConnectionState = ConnectionState.CONNECTING;
-
- mConnectThread = new SocketConnectThread();
- mConnectThread.start();
- }
- }
-
- /**
- * Disconnects from MAS instance
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_CONNECT}
- */
- public void disconnect() {
- if (mConnectThread == null && mObexSession == null) {
- return;
- }
-
- mConnectionState = ConnectionState.DISCONNECTING;
-
- if (mConnectThread != null) {
- mConnectThread.interrupt();
- }
-
- if (mObexSession != null) {
- mObexSession.stop();
- }
- }
-
- @Override
- public void finalize() {
- disconnect();
- }
-
- /**
- * Gets current connection state
- *
- * @return current connection state
- * @see ConnectionState
- */
- public ConnectionState getState() {
- return mConnectionState;
- }
-
- private boolean enableNotifications() {
- Log.v(TAG, "enableNotifications()");
-
- if (mMnsService == null) {
- mMnsService = new BluetoothMnsService();
- }
-
- mMnsService.registerCallback(mMas.getMasInstanceId(), mSessionHandler);
-
- BluetoothMasRequest request = new BluetoothMasRequestSetNotificationRegistration(true);
- return mObexSession.makeRequest(request);
- }
-
- private boolean disableNotifications() {
- Log.v(TAG, "enableNotifications()");
-
- if (mMnsService != null) {
- mMnsService.unregisterCallback(mMas.getMasInstanceId());
- }
-
- mMnsService = null;
-
- BluetoothMasRequest request = new BluetoothMasRequestSetNotificationRegistration(false);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Sets state of notifications for MAS instance
- * <p>
- * Once notifications are enabled, callback handler will receive
- * {@link #EVENT_EVENT_REPORT} when new notification is received
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_SET_NOTIFICATION_REGISTRATION}
- *
- * @param status <code>true</code> if notifications shall be enabled,
- * <code>false</code> otherwise
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean setNotificationRegistration(boolean status) {
- if (mObexSession == null) {
- return false;
- }
-
- if (status) {
- return enableNotifications();
- } else {
- return disableNotifications();
- }
- }
-
- /**
- * Gets current state of notifications for MAS instance
- *
- * @return <code>true</code> if notifications are enabled,
- * <code>false</code> otherwise
- */
- public boolean getNotificationRegistration() {
- return mNotificationEnabled;
- }
-
- /**
- * Goes back to root of folder hierarchy
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_SET_PATH}
- *
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean setFolderRoot() {
- if (mObexSession == null) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestSetPath(true);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Goes back to parent folder in folder hierarchy
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_SET_PATH}
- *
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean setFolderUp() {
- if (mObexSession == null) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestSetPath(false);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Goes down to specified sub-folder in folder hierarchy
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_SET_PATH}
- *
- * @param name name of sub-folder
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean setFolderDown(String name) {
- if (mObexSession == null) {
- return false;
- }
-
- if (name == null || name.isEmpty() || name.contains("/")) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestSetPath(name);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Gets current path in folder hierarchy
- *
- * @return current path
- */
- public String getCurrentPath() {
- if (mPath.size() == 0) {
- return "";
- }
-
- Iterator<String> iter = mPath.iterator();
-
- StringBuilder sb = new StringBuilder(iter.next());
-
- while (iter.hasNext()) {
- sb.append("/").append(iter.next());
- }
-
- return sb.toString();
- }
-
- /**
- * Gets list of sub-folders in current folder
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_GET_FOLDER_LISTING}
- *
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean getFolderListing() {
- return getFolderListing((short) 0, (short) 0);
- }
-
- /**
- * Gets list of sub-folders in current folder
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_GET_FOLDER_LISTING}
- *
- * @param maxListCount maximum number of items returned or <code>0</code>
- * for default value
- * @param listStartOffset index of first item returned or <code>0</code> for
- * default value
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- * @throws IllegalArgumentException if either maxListCount or
- * listStartOffset are outside allowed range [0..65535]
- */
- public boolean getFolderListing(int maxListCount, int listStartOffset) {
- if (mObexSession == null) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestGetFolderListing(maxListCount,
- listStartOffset);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Gets number of sub-folders in current folder
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_GET_FOLDER_LISTING_SIZE}
- *
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean getFolderListingSize() {
- if (mObexSession == null) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestGetFolderListingSize();
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Gets list of messages in specified sub-folder
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_GET_MESSAGES_LISTING}
- *
- * @param folder name of sub-folder or <code>null</code> for current folder
- * @param parameters bit-mask specifying requested parameters in listing or
- * <code>0</code> for default value
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean getMessagesListing(String folder, int parameters) {
- return getMessagesListing(folder, parameters, null, (byte) 0, 0, 0);
- }
-
- /**
- * Gets list of messages in specified sub-folder
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_GET_MESSAGES_LISTING}
- *
- * @param folder name of sub-folder or <code>null</code> for current folder
- * @param parameters corresponds to <code>ParameterMask</code> application
- * parameter in MAP specification
- * @param filter {@link MessagesFilter} object describing filters to be
- * applied on listing by MSE
- * @param subjectLength maximum length of message subject in returned
- * listing or <code>0</code> for default value
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- * @throws IllegalArgumentException if subjectLength is outside allowed
- * range [0..255]
- */
- public boolean getMessagesListing(String folder, int parameters, MessagesFilter filter,
- int subjectLength) {
-
- return getMessagesListing(folder, parameters, filter, subjectLength, 0, 0);
- }
-
- /**
- * Gets list of messages in specified sub-folder
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_GET_MESSAGES_LISTING}
- *
- * @param folder name of sub-folder or <code>null</code> for current folder
- * @param parameters corresponds to <code>ParameterMask</code> application
- * parameter in MAP specification
- * @param filter {@link MessagesFilter} object describing filters to be
- * applied on listing by MSE
- * @param subjectLength maximum length of message subject in returned
- * listing or <code>0</code> for default value
- * @param maxListCount maximum number of items returned or <code>0</code>
- * for default value
- * @param listStartOffset index of first item returned or <code>0</code> for
- * default value
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- * @throws IllegalArgumentException if subjectLength is outside allowed
- * range [0..255] or either maxListCount or listStartOffset are
- * outside allowed range [0..65535]
- */
- public boolean getMessagesListing(String folder, int parameters, MessagesFilter filter,
- int subjectLength, int maxListCount, int listStartOffset) {
-
- if (mObexSession == null) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestGetMessagesListing(folder,
- parameters, filter, subjectLength, maxListCount, listStartOffset);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Gets number of messages in current folder
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_GET_MESSAGES_LISTING_SIZE}
- *
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean getMessagesListingSize() {
- if (mObexSession == null) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestGetMessagesListingSize();
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Retrieves message from MSE
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_GET_MESSAGE}
- *
- * @param handle handle of message to retrieve
- * @param charset {@link CharsetType} object corresponding to
- * <code>Charset</code> application parameter in MAP
- * specification
- * @param attachment corresponds to <code>Attachment</code> application
- * parameter in MAP specification
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean getMessage(String handle, boolean attachment) {
- if (mObexSession == null) {
- return false;
- }
-
- try {
- /* just to validate */
- new BigInteger(handle, 16);
- } catch (NumberFormatException e) {
- return false;
- }
-
- // Since we support only text messaging via Bluetooth, it is OK to restrict the requests to
- // force conversion to UTF-8.
- BluetoothMasRequest request =
- new BluetoothMasRequestGetMessage(handle, CharsetType.UTF_8, attachment);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Sets read status of message on MSE
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_SET_MESSAGE_STATUS}
- *
- * @param handle handle of message
- * @param read <code>true</code> for "read", <code>false</code> for "unread"
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean setMessageReadStatus(String handle, boolean read) {
- if (mObexSession == null) {
- return false;
- }
-
- try {
- /* just to validate */
- new BigInteger(handle, 16);
- } catch (NumberFormatException e) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestSetMessageStatus(handle,
- StatusIndicator.READ, read);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Sets deleted status of message on MSE
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_SET_MESSAGE_STATUS}
- *
- * @param handle handle of message
- * @param deleted <code>true</code> for "deleted", <code>false</code> for
- * "undeleted"
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean setMessageDeletedStatus(String handle, boolean deleted) {
- if (mObexSession == null) {
- return false;
- }
-
- try {
- /* just to validate */
- new BigInteger(handle, 16);
- } catch (NumberFormatException e) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestSetMessageStatus(handle,
- StatusIndicator.DELETED, deleted);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Pushes new message to MSE
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_PUSH_MESSAGE}
- *
- * @param folder name of sub-folder to push to or <code>null</code> for
- * current folder
- * @param charset {@link CharsetType} object corresponding to
- * <code>Charset</code> application parameter in MAP
- * specification
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean pushMessage(String folder, BluetoothMapBmessage bmsg, CharsetType charset) {
- return pushMessage(folder, bmsg, charset, false, false);
- }
-
- /**
- * Pushes new message to MSE
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_PUSH_MESSAGE}
- *
- * @param folder name of sub-folder to push to or <code>null</code> for
- * current folder
- * @param bmsg {@link BluetoothMapBmessage} object representing message to
- * be pushed
- * @param charset {@link CharsetType} object corresponding to
- * <code>Charset</code> application parameter in MAP
- * specification
- * @param transparent corresponds to <code>Transparent</code> application
- * parameter in MAP specification
- * @param retry corresponds to <code>Transparent</code> application
- * parameter in MAP specification
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean pushMessage(String folder, BluetoothMapBmessage bmsg, CharsetType charset,
- boolean transparent, boolean retry) {
- if (mObexSession == null) {
- return false;
- }
-
- String bmsgString = BluetoothMapBmessageBuilder.createBmessage(bmsg);
-
- BluetoothMasRequest request =
- new BluetoothMasRequestPushMessage(folder, bmsgString, charset, transparent, retry);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Requests MSE to initiate ubdate of inbox
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_UPDATE_INBOX}
- *
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean updateInbox() {
- if (mObexSession == null) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestUpdateInbox();
- return mObexSession.makeRequest(request);
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasObexClientSession.java b/android/bluetooth/client/map/BluetoothMasObexClientSession.java
deleted file mode 100644
index 9bf75d4..0000000
--- a/android/bluetooth/client/map/BluetoothMasObexClientSession.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.os.Process;
-import android.util.Log;
-
-import java.io.IOException;
-import java.lang.ref.WeakReference;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-import javax.obex.ObexTransport;
-import javax.obex.ResponseCodes;
-
-class BluetoothMasObexClientSession {
- private static final String TAG = "BluetoothMasObexClientSession";
-
- private static final byte[] MAS_TARGET = new byte[] {
- (byte) 0xbb, 0x58, 0x2b, 0x40, 0x42, 0x0c, 0x11, (byte) 0xdb, (byte) 0xb0, (byte) 0xde,
- 0x08, 0x00, 0x20, 0x0c, (byte) 0x9a, 0x66
- };
-
- private boolean DBG = true;
-
- static final int MSG_OBEX_CONNECTED = 100;
- static final int MSG_OBEX_DISCONNECTED = 101;
- static final int MSG_REQUEST_COMPLETED = 102;
-
- private static final int CONNECT = 0;
- private static final int DISCONNECT = 1;
- private static final int REQUEST = 2;
-
- private final ObexTransport mTransport;
-
- private final Handler mSessionHandler;
-
- private ClientSession mSession;
-
- private HandlerThread mThread;
- private Handler mHandler;
-
- private boolean mConnected;
-
- private static class ObexClientHandler extends Handler {
- WeakReference<BluetoothMasObexClientSession> mInst;
-
- ObexClientHandler(Looper looper, BluetoothMasObexClientSession inst) {
- super(looper);
- mInst = new WeakReference<BluetoothMasObexClientSession>(inst);
- }
-
- @Override
- public void handleMessage(Message msg) {
- BluetoothMasObexClientSession inst = mInst.get();
- if (!inst.connected() && msg.what != CONNECT) {
- Log.w(TAG, "Cannot execute " + msg + " when not CONNECTED.");
- return;
- }
-
- switch (msg.what) {
- case CONNECT:
- inst.connect();
- break;
-
- case DISCONNECT:
- inst.disconnect();
- break;
-
- case REQUEST:
- inst.executeRequest((BluetoothMasRequest) msg.obj);
- break;
- }
- }
- }
-
- public BluetoothMasObexClientSession(ObexTransport transport, Handler handler) {
- mTransport = transport;
- mSessionHandler = handler;
- }
-
- public void start() {
- if (DBG) Log.d(TAG, "start called.");
- if (mConnected) {
- if (DBG) Log.d(TAG, "Already connected, nothing to do.");
- return;
- }
-
- // Start a thread to handle messages here.
- mThread = new HandlerThread("BluetoothMasObexClientSessionThread");
- mThread.start();
- mHandler = new ObexClientHandler(mThread.getLooper(), this);
-
- // Connect it to the target device via OBEX.
- mHandler.obtainMessage(CONNECT).sendToTarget();
- }
-
- public boolean makeRequest(BluetoothMasRequest request) {
- if (DBG) Log.d(TAG, "makeRequest called with: " + request);
-
- boolean status = mHandler.sendMessage(mHandler.obtainMessage(REQUEST, request));
- if (!status) {
- Log.e(TAG, "Adding messages failed, state: " + mConnected);
- return false;
- }
- return true;
- }
-
- public void stop() {
- if (DBG) Log.d(TAG, "stop called...");
-
- mThread.quit();
- disconnect();
- }
-
- private void connect() {
- try {
- mSession = new ClientSession(mTransport);
-
- HeaderSet headerset = new HeaderSet();
- headerset.setHeader(HeaderSet.TARGET, MAS_TARGET);
-
- headerset = mSession.connect(headerset);
-
- if (headerset.getResponseCode() == ResponseCodes.OBEX_HTTP_OK) {
- mConnected = true;
- mSessionHandler.obtainMessage(MSG_OBEX_CONNECTED).sendToTarget();
- } else {
- disconnect();
- }
- } catch (IOException e) {
- disconnect();
- }
- }
-
- private void disconnect() {
- if (mSession != null) {
- try {
- mSession.disconnect(null);
- } catch (IOException e) {
- }
-
- try {
- mSession.close();
- } catch (IOException e) {
- }
- }
-
- mConnected = false;
- mSessionHandler.obtainMessage(MSG_OBEX_DISCONNECTED).sendToTarget();
- }
-
- private void executeRequest(BluetoothMasRequest request) {
- try {
- request.execute(mSession);
- mSessionHandler.obtainMessage(MSG_REQUEST_COMPLETED, request).sendToTarget();
- } catch (IOException e) {
- if (DBG) Log.d(TAG, "Request failed: " + request);
-
- // Disconnect to cleanup.
- disconnect();
- }
- }
-
-
- private boolean connected() {
- return mConnected;
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequest.java b/android/bluetooth/client/map/BluetoothMasRequest.java
deleted file mode 100644
index 0c9c29c..0000000
--- a/android/bluetooth/client/map/BluetoothMasRequest.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-
-import javax.obex.ClientOperation;
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-import javax.obex.Operation;
-import javax.obex.ResponseCodes;
-
-abstract class BluetoothMasRequest {
-
- protected static final byte OAP_TAGID_MAX_LIST_COUNT = 0x01;
- protected static final byte OAP_TAGID_START_OFFSET = 0x02;
- protected static final byte OAP_TAGID_FILTER_MESSAGE_TYPE = 0x03;
- protected static final byte OAP_TAGID_FILTER_PERIOD_BEGIN = 0x04;
- protected static final byte OAP_TAGID_FILTER_PERIOD_END = 0x05;
- protected static final byte OAP_TAGID_FILTER_READ_STATUS = 0x06;
- protected static final byte OAP_TAGID_FILTER_RECIPIENT = 0x07;
- protected static final byte OAP_TAGID_FILTER_ORIGINATOR = 0x08;
- protected static final byte OAP_TAGID_FILTER_PRIORITY = 0x09;
- protected static final byte OAP_TAGID_ATTACHMENT = 0x0a;
- protected static final byte OAP_TAGID_TRANSPARENT = 0xb;
- protected static final byte OAP_TAGID_RETRY = 0xc;
- protected static final byte OAP_TAGID_NEW_MESSAGE = 0x0d;
- protected static final byte OAP_TAGID_NOTIFICATION_STATUS = 0x0e;
- protected static final byte OAP_TAGID_MAS_INSTANCE_ID = 0x0f;
- protected static final byte OAP_TAGID_PARAMETER_MASK = 0x10;
- protected static final byte OAP_TAGID_FOLDER_LISTING_SIZE = 0x11;
- protected static final byte OAP_TAGID_MESSAGES_LISTING_SIZE = 0x12;
- protected static final byte OAP_TAGID_SUBJECT_LENGTH = 0x13;
- protected static final byte OAP_TAGID_CHARSET = 0x14;
- protected static final byte OAP_TAGID_STATUS_INDICATOR = 0x17;
- protected static final byte OAP_TAGID_STATUS_VALUE = 0x18;
- protected static final byte OAP_TAGID_MSE_TIME = 0x19;
-
- protected static byte NOTIFICATION_ON = 0x01;
- protected static byte NOTIFICATION_OFF = 0x00;
-
- protected static byte ATTACHMENT_ON = 0x01;
- protected static byte ATTACHMENT_OFF = 0x00;
-
- protected static byte CHARSET_NATIVE = 0x00;
- protected static byte CHARSET_UTF8 = 0x01;
-
- protected static byte STATUS_INDICATOR_READ = 0x00;
- protected static byte STATUS_INDICATOR_DELETED = 0x01;
-
- protected static byte STATUS_NO = 0x00;
- protected static byte STATUS_YES = 0x01;
-
- protected static byte TRANSPARENT_OFF = 0x00;
- protected static byte TRANSPARENT_ON = 0x01;
-
- protected static byte RETRY_OFF = 0x00;
- protected static byte RETRY_ON = 0x01;
-
- /* used for PUT requests which require filler byte */
- protected static final byte[] FILLER_BYTE = {
- 0x30
- };
-
- protected HeaderSet mHeaderSet;
-
- protected int mResponseCode;
-
- public BluetoothMasRequest() {
- mHeaderSet = new HeaderSet();
- }
-
- abstract public void execute(ClientSession session) throws IOException;
-
- protected void executeGet(ClientSession session) throws IOException {
- ClientOperation op = null;
-
- try {
- op = (ClientOperation) session.get(mHeaderSet);
-
- /*
- * MAP spec does not explicitly require that GET request should be
- * sent in single packet but for some reason PTS complains when
- * final GET packet with no headers follows non-final GET with all
- * headers. So this is workaround, at least temporary. TODO: check
- * with PTS
- */
- op.setGetFinalFlag(true);
-
- /*
- * this will trigger ClientOperation to use non-buffered stream so
- * we can abort operation
- */
- op.continueOperation(true, false);
-
- readResponseHeaders(op.getReceivedHeader());
-
- InputStream is = op.openInputStream();
- readResponse(is);
- is.close();
-
- op.close();
-
- mResponseCode = op.getResponseCode();
- } catch (IOException e) {
- mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
-
- throw e;
- }
- }
-
- protected void executePut(ClientSession session, byte[] body) throws IOException {
- Operation op = null;
-
- mHeaderSet.setHeader(HeaderSet.LENGTH, Long.valueOf(body.length));
-
- try {
- op = session.put(mHeaderSet);
-
- DataOutputStream out = op.openDataOutputStream();
- out.write(body);
- out.close();
-
- readResponseHeaders(op.getReceivedHeader());
-
- op.close();
- mResponseCode = op.getResponseCode();
- } catch (IOException e) {
- mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
-
- throw e;
- }
- }
-
- final public boolean isSuccess() {
- return (mResponseCode == ResponseCodes.OBEX_HTTP_OK);
- }
-
- protected void readResponse(InputStream stream) throws IOException {
- /* nothing here by default */
- }
-
- protected void readResponseHeaders(HeaderSet headerset) {
- /* nothing here by default */
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestGetFolderListing.java b/android/bluetooth/client/map/BluetoothMasRequestGetFolderListing.java
deleted file mode 100644
index db22ada..0000000
--- a/android/bluetooth/client/map/BluetoothMasRequestGetFolderListing.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-import android.bluetooth.client.map.utils.ObexAppParameters;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-
-final class BluetoothMasRequestGetFolderListing extends BluetoothMasRequest {
-
- private static final String TYPE = "x-obex/folder-listing";
-
- private BluetoothMapFolderListing mResponse = null;
-
- public BluetoothMasRequestGetFolderListing(int maxListCount, int listStartOffset) {
-
- if (maxListCount < 0 || maxListCount > 65535) {
- throw new IllegalArgumentException("maxListCount should be [0..65535]");
- }
-
- if (listStartOffset < 0 || listStartOffset > 65535) {
- throw new IllegalArgumentException("listStartOffset should be [0..65535]");
- }
-
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
-
- ObexAppParameters oap = new ObexAppParameters();
- // Allow GetFolderListing for maxListCount value 0 also.
- if (maxListCount >= 0) {
- oap.add(OAP_TAGID_MAX_LIST_COUNT, (short) maxListCount);
- }
-
- if (listStartOffset > 0) {
- oap.add(OAP_TAGID_START_OFFSET, (short) listStartOffset);
- }
-
- oap.addToHeaderSet(mHeaderSet);
- }
-
- @Override
- protected void readResponse(InputStream stream) {
- mResponse = new BluetoothMapFolderListing(stream);
- }
-
- public ArrayList<String> getList() {
- if (mResponse == null) {
- return null;
- }
-
- return mResponse.getList();
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executeGet(session);
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestGetFolderListingSize.java b/android/bluetooth/client/map/BluetoothMasRequestGetFolderListingSize.java
deleted file mode 100644
index 910c036..0000000
--- a/android/bluetooth/client/map/BluetoothMasRequestGetFolderListingSize.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import android.bluetooth.client.map.utils.ObexAppParameters;
-
-import java.io.IOException;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-
-final class BluetoothMasRequestGetFolderListingSize extends BluetoothMasRequest {
-
- private static final String TYPE = "x-obex/folder-listing";
-
- private int mSize;
-
- public BluetoothMasRequestGetFolderListingSize() {
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
-
- ObexAppParameters oap = new ObexAppParameters();
- oap.add(OAP_TAGID_MAX_LIST_COUNT, 0);
-
- oap.addToHeaderSet(mHeaderSet);
- }
-
- @Override
- protected void readResponseHeaders(HeaderSet headerset) {
- ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
-
- mSize = oap.getShort(OAP_TAGID_FOLDER_LISTING_SIZE);
- }
-
- public int getSize() {
- return mSize;
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executeGet(session);
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestGetMessage.java b/android/bluetooth/client/map/BluetoothMasRequestGetMessage.java
deleted file mode 100644
index 923bff0..0000000
--- a/android/bluetooth/client/map/BluetoothMasRequestGetMessage.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import android.util.Log;
-
-
-import android.bluetooth.client.map.BluetoothMasClient.CharsetType;
-import android.bluetooth.client.map.utils.ObexAppParameters;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import java.nio.charset.StandardCharsets;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-import javax.obex.ResponseCodes;
-
-final class BluetoothMasRequestGetMessage extends BluetoothMasRequest {
-
- private static final String TAG = "BluetoothMasRequestGetMessage";
-
- private static final String TYPE = "x-bt/message";
-
- private BluetoothMapBmessage mBmessage;
-
- public BluetoothMasRequestGetMessage(String handle, CharsetType charset, boolean attachment) {
-
- mHeaderSet.setHeader(HeaderSet.NAME, handle);
-
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
-
- ObexAppParameters oap = new ObexAppParameters();
-
- oap.add(OAP_TAGID_CHARSET, CharsetType.UTF_8.equals(charset) ? CHARSET_UTF8
- : CHARSET_NATIVE);
-
- oap.add(OAP_TAGID_ATTACHMENT, attachment ? ATTACHMENT_ON : ATTACHMENT_OFF);
-
- oap.addToHeaderSet(mHeaderSet);
- }
-
- @Override
- protected void readResponse(InputStream stream) {
-
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- byte[] buf = new byte[1024];
-
- try {
- int len;
- while ((len = stream.read(buf)) != -1) {
- baos.write(buf, 0, len);
- }
- } catch (IOException e) {
- Log.e(TAG, "I/O exception while reading response", e);
- }
-
- // Convert the input stream using UTF-8 since the attributes in the payload are all encoded
- // according to it. The actual message body may need to be transcoded depending on
- // charset/encoding defined for body-content.
- String bmsg;
- try {
- bmsg = baos.toString(StandardCharsets.UTF_8.name());
- } catch (UnsupportedEncodingException ex) {
- Log.e(TAG,
- "Coudn't decode the bmessage with UTF-8. Something must be really messed up.");
- return;
- }
-
- mBmessage = BluetoothMapBmessageParser.createBmessage(bmsg);
-
- if (mBmessage == null) {
- mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
- }
- }
-
- public BluetoothMapBmessage getMessage() {
- return mBmessage;
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executeGet(session);
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListing.java b/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListing.java
deleted file mode 100644
index 2ad167d..0000000
--- a/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListing.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import android.bluetooth.client.map.BluetoothMasClient.MessagesFilter;
-import android.bluetooth.client.map.utils.ObexAppParameters;
-import android.bluetooth.client.map.utils.ObexTime;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.Date;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-
-final class BluetoothMasRequestGetMessagesListing extends BluetoothMasRequest {
-
- private static final String TYPE = "x-bt/MAP-msg-listing";
-
- private BluetoothMapMessagesListing mResponse = null;
-
- private boolean mNewMessage = false;
-
- private Date mServerTime = null;
-
- public BluetoothMasRequestGetMessagesListing(String folderName, int parameters,
- BluetoothMasClient.MessagesFilter filter, int subjectLength, int maxListCount,
- int listStartOffset) {
- if (subjectLength < 0 || subjectLength > 255) {
- throw new IllegalArgumentException("subjectLength should be [0..255]");
- }
-
- if (maxListCount < 0 || maxListCount > 65535) {
- throw new IllegalArgumentException("maxListCount should be [0..65535]");
- }
-
- if (listStartOffset < 0 || listStartOffset > 65535) {
- throw new IllegalArgumentException("listStartOffset should be [0..65535]");
- }
-
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
-
- if (folderName == null) {
- mHeaderSet.setHeader(HeaderSet.NAME, "");
- } else {
- mHeaderSet.setHeader(HeaderSet.NAME, folderName);
- }
-
- ObexAppParameters oap = new ObexAppParameters();
-
- if (filter != null) {
- if (filter.messageType != MessagesFilter.MESSAGE_TYPE_ALL) {
- oap.add(OAP_TAGID_FILTER_MESSAGE_TYPE, filter.messageType);
- }
-
- if (filter.periodBegin != null) {
- oap.add(OAP_TAGID_FILTER_PERIOD_BEGIN, filter.periodBegin);
- }
-
- if (filter.periodEnd != null) {
- oap.add(OAP_TAGID_FILTER_PERIOD_END, filter.periodEnd);
- }
-
- if (filter.readStatus != MessagesFilter.READ_STATUS_ANY) {
- oap.add(OAP_TAGID_FILTER_READ_STATUS, filter.readStatus);
- }
-
- if (filter.recipient != null) {
- oap.add(OAP_TAGID_FILTER_RECIPIENT, filter.recipient);
- }
-
- if (filter.originator != null) {
- oap.add(OAP_TAGID_FILTER_ORIGINATOR, filter.originator);
- }
-
- if (filter.priority != MessagesFilter.PRIORITY_ANY) {
- oap.add(OAP_TAGID_FILTER_PRIORITY, filter.priority);
- }
- }
-
- if (subjectLength != 0) {
- oap.add(OAP_TAGID_SUBJECT_LENGTH, (byte) subjectLength);
- }
- /* Include parameterMask only when specific values are selected,
- * to avoid IOT specific issue with no paramterMask header support.
- */
- if (parameters > 0 ) {
- oap.add(OAP_TAGID_PARAMETER_MASK, parameters);
- }
- // Allow GetMessageListing for maxlistcount value 0 also.
- if (maxListCount >= 0) {
- oap.add(OAP_TAGID_MAX_LIST_COUNT, (short) maxListCount);
- }
-
- if (listStartOffset != 0) {
- oap.add(OAP_TAGID_START_OFFSET, (short) listStartOffset);
- }
-
- oap.addToHeaderSet(mHeaderSet);
- }
-
- @Override
- protected void readResponse(InputStream stream) {
- mResponse = new BluetoothMapMessagesListing(stream);
- }
-
- @Override
- protected void readResponseHeaders(HeaderSet headerset) {
- ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
-
- mNewMessage = ((oap.getByte(OAP_TAGID_NEW_MESSAGE) & 0x01) == 1);
-
- if (oap.exists(OAP_TAGID_MSE_TIME)) {
- String mseTime = oap.getString(OAP_TAGID_MSE_TIME);
- if(mseTime != null )
- mServerTime = (new ObexTime(mseTime)).getTime();
- }
- }
-
- public ArrayList<BluetoothMapMessage> getList() {
- if (mResponse == null) {
- return null;
- }
-
- return mResponse.getList();
- }
-
- public boolean getNewMessageStatus() {
- return mNewMessage;
- }
-
- public Date getMseTime() {
- return mServerTime;
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executeGet(session);
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListingSize.java b/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListingSize.java
deleted file mode 100644
index cdadb2e..0000000
--- a/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListingSize.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import android.bluetooth.client.map.utils.ObexAppParameters;
-
-import java.io.IOException;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-
-final class BluetoothMasRequestGetMessagesListingSize extends BluetoothMasRequest {
-
- private static final String TYPE = "x-bt/MAP-msg-listing";
-
- private int mSize;
-
- public BluetoothMasRequestGetMessagesListingSize() {
- mHeaderSet.setHeader(HeaderSet.NAME, "");
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
-
- ObexAppParameters oap = new ObexAppParameters();
- oap.add(OAP_TAGID_MAX_LIST_COUNT, (short) 0);
-
- oap.addToHeaderSet(mHeaderSet);
- }
-
- @Override
- protected void readResponseHeaders(HeaderSet headerset) {
- ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
-
- mSize = oap.getShort(OAP_TAGID_MESSAGES_LISTING_SIZE);
- }
-
- public int getSize() {
- return mSize;
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executeGet(session);
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestPushMessage.java b/android/bluetooth/client/map/BluetoothMasRequestPushMessage.java
deleted file mode 100644
index 8fc9bd4..0000000
--- a/android/bluetooth/client/map/BluetoothMasRequestPushMessage.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import java.io.IOException;
-import java.math.BigInteger;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-import javax.obex.ResponseCodes;
-
-import android.bluetooth.client.map.BluetoothMasClient.CharsetType;
-import android.bluetooth.client.map.utils.ObexAppParameters;
-
-final class BluetoothMasRequestPushMessage extends BluetoothMasRequest {
-
- private static final String TYPE = "x-bt/message";
- private String mMsg;
- private String mMsgHandle;
-
- private BluetoothMasRequestPushMessage(String folder) {
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
- if (folder == null) {
- folder = "";
- }
- mHeaderSet.setHeader(HeaderSet.NAME, folder);
- }
-
- public BluetoothMasRequestPushMessage(String folder, String msg, CharsetType charset,
- boolean transparent, boolean retry) {
- this(folder);
- mMsg = msg;
- ObexAppParameters oap = new ObexAppParameters();
- oap.add(OAP_TAGID_TRANSPARENT, transparent ? TRANSPARENT_ON : TRANSPARENT_OFF);
- oap.add(OAP_TAGID_RETRY, retry ? RETRY_ON : RETRY_OFF);
- oap.add(OAP_TAGID_CHARSET, charset == CharsetType.NATIVE ? CHARSET_NATIVE : CHARSET_UTF8);
- oap.addToHeaderSet(mHeaderSet);
- }
-
- @Override
- protected void readResponseHeaders(HeaderSet headerset) {
- try {
- String handle = (String) headerset.getHeader(HeaderSet.NAME);
- if (handle != null) {
- /* just to validate */
- new BigInteger(handle, 16);
-
- mMsgHandle = handle;
- }
- } catch (NumberFormatException e) {
- mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
- } catch (IOException e) {
- mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
- }
- }
-
- public String getMsgHandle() {
- return mMsgHandle;
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executePut(session, mMsg.getBytes());
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestSetMessageStatus.java b/android/bluetooth/client/map/BluetoothMasRequestSetMessageStatus.java
deleted file mode 100644
index 140312e..0000000
--- a/android/bluetooth/client/map/BluetoothMasRequestSetMessageStatus.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import android.bluetooth.client.map.utils.ObexAppParameters;
-
-import java.io.IOException;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-
-final class BluetoothMasRequestSetMessageStatus extends BluetoothMasRequest {
-
- public enum StatusIndicator {
- READ, DELETED;
- }
-
- private static final String TYPE = "x-bt/messageStatus";
-
- public BluetoothMasRequestSetMessageStatus(String handle, StatusIndicator statusInd,
- boolean statusValue) {
-
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
- mHeaderSet.setHeader(HeaderSet.NAME, handle);
-
- ObexAppParameters oap = new ObexAppParameters();
- oap.add(OAP_TAGID_STATUS_INDICATOR,
- statusInd == StatusIndicator.READ ? STATUS_INDICATOR_READ
- : STATUS_INDICATOR_DELETED);
- oap.add(OAP_TAGID_STATUS_VALUE, statusValue ? STATUS_YES : STATUS_NO);
- oap.addToHeaderSet(mHeaderSet);
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executePut(session, FILLER_BYTE);
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestSetNotificationRegistration.java b/android/bluetooth/client/map/BluetoothMasRequestSetNotificationRegistration.java
deleted file mode 100644
index debb508..0000000
--- a/android/bluetooth/client/map/BluetoothMasRequestSetNotificationRegistration.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import android.bluetooth.client.map.utils.ObexAppParameters;
-
-import java.io.IOException;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-
-final class BluetoothMasRequestSetNotificationRegistration extends BluetoothMasRequest {
-
- private static final String TYPE = "x-bt/MAP-NotificationRegistration";
-
- private final boolean mStatus;
-
- public BluetoothMasRequestSetNotificationRegistration(boolean status) {
- mStatus = status;
-
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
-
- ObexAppParameters oap = new ObexAppParameters();
-
- oap.add(OAP_TAGID_NOTIFICATION_STATUS, status ? NOTIFICATION_ON : NOTIFICATION_OFF);
-
- oap.addToHeaderSet(mHeaderSet);
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executePut(session, FILLER_BYTE);
- }
-
- public boolean getStatus() {
- return mStatus;
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestSetPath.java b/android/bluetooth/client/map/BluetoothMasRequestSetPath.java
deleted file mode 100644
index 71e2dbe..0000000
--- a/android/bluetooth/client/map/BluetoothMasRequestSetPath.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import java.io.IOException;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-import javax.obex.ResponseCodes;
-
-class BluetoothMasRequestSetPath extends BluetoothMasRequest {
-
- enum SetPathDir {
- ROOT, UP, DOWN
- };
-
- SetPathDir mDir;
-
- String mName;
-
- public BluetoothMasRequestSetPath(String name) {
- mDir = SetPathDir.DOWN;
- mName = name;
-
- mHeaderSet.setHeader(HeaderSet.NAME, name);
- }
-
- public BluetoothMasRequestSetPath(boolean goRoot) {
- mHeaderSet.setEmptyNameHeader();
- if (goRoot) {
- mDir = SetPathDir.ROOT;
- } else {
- mDir = SetPathDir.UP;
- }
- }
-
- @Override
- public void execute(ClientSession session) {
- HeaderSet hs = null;
-
- try {
- switch (mDir) {
- case ROOT:
- case DOWN:
- hs = session.setPath(mHeaderSet, false, false);
- break;
- case UP:
- hs = session.setPath(mHeaderSet, true, false);
- break;
- }
-
- mResponseCode = hs.getResponseCode();
- } catch (IOException e) {
- mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
- }
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestUpdateInbox.java b/android/bluetooth/client/map/BluetoothMasRequestUpdateInbox.java
deleted file mode 100644
index aeec632..0000000
--- a/android/bluetooth/client/map/BluetoothMasRequestUpdateInbox.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import java.io.IOException;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-
-final class BluetoothMasRequestUpdateInbox extends BluetoothMasRequest {
-
- private static final String TYPE = "x-bt/MAP-messageUpdate";
-
- public BluetoothMasRequestUpdateInbox() {
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executePut(session, FILLER_BYTE);
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMnsObexServer.java b/android/bluetooth/client/map/BluetoothMnsObexServer.java
deleted file mode 100644
index 672e9cf..0000000
--- a/android/bluetooth/client/map/BluetoothMnsObexServer.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import android.os.Handler;
-import android.util.Log;
-
-import android.bluetooth.client.map.utils.ObexAppParameters;
-
-import java.io.IOException;
-import java.util.Arrays;
-
-import javax.obex.HeaderSet;
-import javax.obex.Operation;
-import javax.obex.ResponseCodes;
-import javax.obex.ServerRequestHandler;
-
-class BluetoothMnsObexServer extends ServerRequestHandler {
-
- private final static String TAG = "BluetoothMnsObexServer";
-
- private static final byte[] MNS_TARGET = new byte[] {
- (byte) 0xbb, 0x58, 0x2b, 0x41, 0x42, 0x0c, 0x11, (byte) 0xdb, (byte) 0xb0, (byte) 0xde,
- 0x08, 0x00, 0x20, 0x0c, (byte) 0x9a, 0x66
- };
-
- private final static String TYPE = "x-bt/MAP-event-report";
-
- private final Handler mCallback;
-
- public BluetoothMnsObexServer(Handler callback) {
- super();
-
- mCallback = callback;
- }
-
- @Override
- public int onConnect(final HeaderSet request, HeaderSet reply) {
- Log.v(TAG, "onConnect");
-
- try {
- byte[] uuid = (byte[]) request.getHeader(HeaderSet.TARGET);
-
- if (!Arrays.equals(uuid, MNS_TARGET)) {
- return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
- }
-
- } catch (IOException e) {
- // this should never happen since getHeader won't throw exception it
- // declares to throw
- return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
- }
-
- reply.setHeader(HeaderSet.WHO, MNS_TARGET);
- return ResponseCodes.OBEX_HTTP_OK;
- }
-
- @Override
- public void onDisconnect(final HeaderSet request, HeaderSet reply) {
- Log.v(TAG, "onDisconnect");
- }
-
- @Override
- public int onGet(final Operation op) {
- Log.v(TAG, "onGet");
-
- return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
- }
-
- @Override
- public int onPut(final Operation op) {
- Log.v(TAG, "onPut");
-
- try {
- HeaderSet headerset;
- headerset = op.getReceivedHeader();
-
- String type = (String) headerset.getHeader(HeaderSet.TYPE);
- ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
-
- if (!TYPE.equals(type) || !oap.exists(BluetoothMasRequest.OAP_TAGID_MAS_INSTANCE_ID)) {
- return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
- }
-
- Byte inst = oap.getByte(BluetoothMasRequest.OAP_TAGID_MAS_INSTANCE_ID);
-
- BluetoothMapEventReport ev = BluetoothMapEventReport.fromStream(op
- .openDataInputStream());
-
- op.close();
-
- mCallback.obtainMessage(BluetoothMnsService.MSG_EVENT, inst, 0, ev).sendToTarget();
- } catch (IOException e) {
- Log.e(TAG, "I/O exception when handling PUT request", e);
- return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
- }
-
- return ResponseCodes.OBEX_HTTP_OK;
- }
-
- @Override
- public int onAbort(final HeaderSet request, HeaderSet reply) {
- Log.v(TAG, "onAbort");
-
- return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED;
- }
-
- @Override
- public int onSetPath(final HeaderSet request, HeaderSet reply,
- final boolean backup, final boolean create) {
- Log.v(TAG, "onSetPath");
-
- return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
- }
-
- @Override
- public void onClose() {
- Log.v(TAG, "onClose");
-
- // TODO: call session handler so it can disconnect
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMnsService.java b/android/bluetooth/client/map/BluetoothMnsService.java
deleted file mode 100644
index 42175e0..0000000
--- a/android/bluetooth/client/map/BluetoothMnsService.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothServerSocket;
-import android.bluetooth.BluetoothSocket;
-import android.os.Handler;
-import android.os.Message;
-import android.os.ParcelUuid;
-import android.util.Log;
-import android.util.SparseArray;
-
-import java.io.IOException;
-import java.io.InterruptedIOException;
-import java.lang.ref.WeakReference;
-
-import javax.obex.ServerSession;
-
-class BluetoothMnsService {
-
- private static final String TAG = "BluetoothMnsService";
-
- private static final ParcelUuid MAP_MNS =
- ParcelUuid.fromString("00001133-0000-1000-8000-00805F9B34FB");
-
- static final int MSG_EVENT = 1;
-
- /* for BluetoothMasClient */
- static final int EVENT_REPORT = 1001;
-
- /* these are shared across instances */
- static private SparseArray<Handler> mCallbacks = null;
- static private SocketAcceptThread mAcceptThread = null;
- static private Handler mSessionHandler = null;
- static private BluetoothServerSocket mServerSocket = null;
-
- private static class SessionHandler extends Handler {
-
- private final WeakReference<BluetoothMnsService> mService;
-
- SessionHandler(BluetoothMnsService service) {
- mService = new WeakReference<BluetoothMnsService>(service);
- }
-
- @Override
- public void handleMessage(Message msg) {
- Log.d(TAG, "Handler: msg: " + msg.what);
-
- switch (msg.what) {
- case MSG_EVENT:
- int instanceId = msg.arg1;
-
- synchronized (mCallbacks) {
- Handler cb = mCallbacks.get(instanceId);
-
- if (cb != null) {
- BluetoothMapEventReport ev = (BluetoothMapEventReport) msg.obj;
- cb.obtainMessage(EVENT_REPORT, ev).sendToTarget();
- } else {
- Log.w(TAG, "Got event for instance which is not registered: "
- + instanceId);
- }
- }
- break;
- }
- }
- }
-
- private static class SocketAcceptThread extends Thread {
-
- private boolean mInterrupted = false;
-
- @Override
- public void run() {
-
- if (mServerSocket != null) {
- Log.w(TAG, "Socket already created, exiting");
- return;
- }
-
- try {
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- mServerSocket = adapter.listenUsingEncryptedRfcommWithServiceRecord(
- "MAP Message Notification Service", MAP_MNS.getUuid());
- } catch (IOException e) {
- mInterrupted = true;
- Log.e(TAG, "I/O exception when trying to create server socket", e);
- }
-
- while (!mInterrupted) {
- try {
- Log.v(TAG, "waiting to accept connection...");
-
- BluetoothSocket sock = mServerSocket.accept();
-
- Log.v(TAG, "new incoming connection from "
- + sock.getRemoteDevice().getName());
-
- // session will live until closed by remote
- BluetoothMnsObexServer srv = new BluetoothMnsObexServer(mSessionHandler);
- BluetoothMapRfcommTransport transport = new BluetoothMapRfcommTransport(
- sock);
- new ServerSession(transport, srv, null);
- } catch (IOException ex) {
- Log.v(TAG, "I/O exception when waiting to accept (aborted?)");
- mInterrupted = true;
- }
- }
-
- if (mServerSocket != null) {
- try {
- mServerSocket.close();
- } catch (IOException e) {
- // do nothing
- }
-
- mServerSocket = null;
- }
- }
- }
-
- BluetoothMnsService() {
- Log.v(TAG, "BluetoothMnsService()");
-
- if (mCallbacks == null) {
- Log.v(TAG, "BluetoothMnsService(): allocating callbacks");
- mCallbacks = new SparseArray<Handler>();
- }
-
- if (mSessionHandler == null) {
- Log.v(TAG, "BluetoothMnsService(): allocating session handler");
- mSessionHandler = new SessionHandler(this);
- }
- }
-
- public void registerCallback(int instanceId, Handler callback) {
- Log.v(TAG, "registerCallback()");
-
- synchronized (mCallbacks) {
- mCallbacks.put(instanceId, callback);
-
- if (mAcceptThread == null) {
- Log.v(TAG, "registerCallback(): starting MNS server");
- mAcceptThread = new SocketAcceptThread();
- mAcceptThread.setName("BluetoothMnsAcceptThread");
- mAcceptThread.start();
- }
- }
- }
-
- public void unregisterCallback(int instanceId) {
- Log.v(TAG, "unregisterCallback()");
-
- synchronized (mCallbacks) {
- mCallbacks.remove(instanceId);
-
- if (mCallbacks.size() == 0) {
- Log.v(TAG, "unregisterCallback(): shutting down MNS server");
-
- if (mServerSocket != null) {
- try {
- mServerSocket.close();
- } catch (IOException e) {
- }
-
- mServerSocket = null;
- }
-
- mAcceptThread.interrupt();
-
- try {
- mAcceptThread.join(5000);
- } catch (InterruptedException e) {
- }
-
- mAcceptThread = null;
- }
- }
- }
-}
diff --git a/android/bluetooth/client/map/utils/BmsgTokenizer.java b/android/bluetooth/client/map/utils/BmsgTokenizer.java
deleted file mode 100644
index 9f23961..0000000
--- a/android/bluetooth/client/map/utils/BmsgTokenizer.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map.utils;
-
-import android.util.Log;
-
-import java.text.ParseException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public final class BmsgTokenizer {
-
- private final String mStr;
-
- private final Matcher mMatcher;
-
- private int mPos = 0;
-
- private final int mOffset;
-
- static public class Property {
- public final String name;
- public final String value;
-
- public Property(String name, String value) {
- if (name == null || value == null) {
- throw new IllegalArgumentException();
- }
-
- this.name = name;
- this.value = value;
-
- Log.v("BMSG >> ", toString());
- }
-
- @Override
- public String toString() {
- return name + ":" + value;
- }
-
- @Override
- public boolean equals(Object o) {
- return ((o instanceof Property) && ((Property) o).name.equals(name) && ((Property) o).value
- .equals(value));
- }
- };
-
- public BmsgTokenizer(String str) {
- this(str, 0);
- }
-
- public BmsgTokenizer(String str, int offset) {
- mStr = str;
- mOffset = offset;
- mMatcher = Pattern.compile("(([^:]*):(.*))?\r\n").matcher(str);
- mPos = mMatcher.regionStart();
- }
-
- public Property next(boolean alwaysReturn) throws ParseException {
- boolean found = false;
-
- do {
- mMatcher.region(mPos, mMatcher.regionEnd());
-
- if (!mMatcher.lookingAt()) {
- if (alwaysReturn) {
- return null;
- }
-
- throw new ParseException("Property or empty line expected", pos());
- }
-
- mPos = mMatcher.end();
-
- if (mMatcher.group(1) != null) {
- found = true;
- }
- } while (!found);
-
- return new Property(mMatcher.group(2), mMatcher.group(3));
- }
-
- public Property next() throws ParseException {
- return next(false);
- }
-
- public String remaining() {
- return mStr.substring(mPos);
- }
-
- public int pos() {
- return mPos + mOffset;
- }
-}
diff --git a/android/bluetooth/client/map/utils/ObexAppParameters.java b/android/bluetooth/client/map/utils/ObexAppParameters.java
deleted file mode 100644
index cae379b..0000000
--- a/android/bluetooth/client/map/utils/ObexAppParameters.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map.utils;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.HashMap;
-import java.util.Map;
-
-import javax.obex.HeaderSet;
-
-public final class ObexAppParameters {
-
- private final HashMap<Byte, byte[]> mParams;
-
- public ObexAppParameters() {
- mParams = new HashMap<Byte, byte[]>();
- }
-
- public ObexAppParameters(byte[] raw) {
- mParams = new HashMap<Byte, byte[]>();
-
- if (raw != null) {
- for (int i = 0; i < raw.length;) {
- if (raw.length - i < 2) {
- break;
- }
-
- byte tag = raw[i++];
- byte len = raw[i++];
-
- if (raw.length - i - len < 0) {
- break;
- }
-
- byte[] val = new byte[len];
-
- System.arraycopy(raw, i, val, 0, len);
- this.add(tag, val);
-
- i += len;
- }
- }
- }
-
- public static ObexAppParameters fromHeaderSet(HeaderSet headerset) {
- try {
- byte[] raw = (byte[]) headerset.getHeader(HeaderSet.APPLICATION_PARAMETER);
- return new ObexAppParameters(raw);
- } catch (IOException e) {
- // won't happen
- }
-
- return null;
- }
-
- public byte[] getHeader() {
- int length = 0;
-
- for (Map.Entry<Byte, byte[]> entry : mParams.entrySet()) {
- length += (entry.getValue().length + 2);
- }
-
- byte[] ret = new byte[length];
-
- int idx = 0;
- for (Map.Entry<Byte, byte[]> entry : mParams.entrySet()) {
- length = entry.getValue().length;
-
- ret[idx++] = entry.getKey();
- ret[idx++] = (byte) length;
- System.arraycopy(entry.getValue(), 0, ret, idx, length);
- idx += length;
- }
-
- return ret;
- }
-
- public void addToHeaderSet(HeaderSet headerset) {
- if (mParams.size() > 0) {
- headerset.setHeader(HeaderSet.APPLICATION_PARAMETER, getHeader());
- }
- }
-
- public boolean exists(byte tag) {
- return mParams.containsKey(tag);
- }
-
- public void add(byte tag, byte val) {
- byte[] bval = ByteBuffer.allocate(1).put(val).array();
- mParams.put(tag, bval);
- }
-
- public void add(byte tag, short val) {
- byte[] bval = ByteBuffer.allocate(2).putShort(val).array();
- mParams.put(tag, bval);
- }
-
- public void add(byte tag, int val) {
- byte[] bval = ByteBuffer.allocate(4).putInt(val).array();
- mParams.put(tag, bval);
- }
-
- public void add(byte tag, long val) {
- byte[] bval = ByteBuffer.allocate(8).putLong(val).array();
- mParams.put(tag, bval);
- }
-
- public void add(byte tag, String val) {
- byte[] bval = val.getBytes();
- mParams.put(tag, bval);
- }
-
- public void add(byte tag, byte[] bval) {
- mParams.put(tag, bval);
- }
-
- public byte getByte(byte tag) {
- byte[] bval = mParams.get(tag);
-
- if (bval == null || bval.length < 1) {
- return 0;
- }
-
- return ByteBuffer.wrap(bval).get();
- }
-
- public short getShort(byte tag) {
- byte[] bval = mParams.get(tag);
-
- if (bval == null || bval.length < 2) {
- return 0;
- }
-
- return ByteBuffer.wrap(bval).getShort();
- }
-
- public int getInt(byte tag) {
- byte[] bval = mParams.get(tag);
-
- if (bval == null || bval.length < 4) {
- return 0;
- }
-
- return ByteBuffer.wrap(bval).getInt();
- }
-
- public String getString(byte tag) {
- byte[] bval = mParams.get(tag);
-
- if (bval == null) {
- return null;
- }
-
- return new String(bval);
- }
-
- public byte[] getByteArray(byte tag) {
- byte[] bval = mParams.get(tag);
-
- return bval;
- }
-
- @Override
- public String toString() {
- return mParams.toString();
- }
-}
diff --git a/android/bluetooth/client/map/utils/ObexTime.java b/android/bluetooth/client/map/utils/ObexTime.java
deleted file mode 100644
index b35ce81..0000000
--- a/android/bluetooth/client/map/utils/ObexTime.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2014 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.bluetooth.client.map.utils;
-
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public final class ObexTime {
-
- private Date mDate;
-
- public ObexTime(String time) {
- /*
- * match OBEX time string: YYYYMMDDTHHMMSS with optional UTF offset
- * +/-hhmm
- */
- Pattern p = Pattern
- .compile("(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})(\\d{2})(([+-])(\\d{2})(\\d{2}))?");
- Matcher m = p.matcher(time);
-
- if (m.matches()) {
-
- /*
- * matched groups are numberes as follows: YYYY MM DD T HH MM SS +
- * hh mm ^^^^ ^^ ^^ ^^ ^^ ^^ ^ ^^ ^^ 1 2 3 4 5 6 8 9 10 all groups
- * are guaranteed to be numeric so conversion will always succeed
- * (except group 8 which is either + or -)
- */
-
- Calendar cal = Calendar.getInstance();
- cal.set(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)) - 1,
- Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)),
- Integer.parseInt(m.group(5)), Integer.parseInt(m.group(6)));
-
- /*
- * if 7th group is matched then we have UTC offset information
- * included
- */
- if (m.group(7) != null) {
- int ohh = Integer.parseInt(m.group(9));
- int omm = Integer.parseInt(m.group(10));
-
- /* time zone offset is specified in miliseconds */
- int offset = (ohh * 60 + omm) * 60 * 1000;
-
- if (m.group(8).equals("-")) {
- offset = -offset;
- }
-
- TimeZone tz = TimeZone.getTimeZone("UTC");
- tz.setRawOffset(offset);
-
- cal.setTimeZone(tz);
- }
-
- mDate = cal.getTime();
- }
- }
-
- public ObexTime(Date date) {
- mDate = date;
- }
-
- public Date getTime() {
- return mDate;
- }
-
- @Override
- public String toString() {
- if (mDate == null) {
- return null;
- }
-
- Calendar cal = Calendar.getInstance();
- cal.setTime(mDate);
-
- /* note that months are numbered stating from 0 */
- return String.format(Locale.US, "%04d%02d%02dT%02d%02d%02d",
- cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1,
- cal.get(Calendar.DATE), cal.get(Calendar.HOUR_OF_DAY),
- cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND));
- }
-}
diff --git a/android/bluetooth/le/BluetoothLeScanner.java b/android/bluetooth/le/BluetoothLeScanner.java
index a189e27..347fc4d 100644
--- a/android/bluetooth/le/BluetoothLeScanner.java
+++ b/android/bluetooth/le/BluetoothLeScanner.java
@@ -387,7 +387,7 @@
if (mScannerId > 0) {
mLeScanClients.put(mScanCallback, this);
} else {
- // Registration timed out or got exception, reset scannerId to -1 so no
+ // Registration timed out or got exception, reset RscannerId to -1 so no
// subsequent operations can proceed.
if (mScannerId == 0) mScannerId = -1;
diff --git a/android/content/ClipData.java b/android/content/ClipData.java
index 9323261..94e1e2d 100644
--- a/android/content/ClipData.java
+++ b/android/content/ClipData.java
@@ -34,16 +34,18 @@
import android.text.TextUtils;
import android.text.style.URLSpan;
import android.util.Log;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.util.ArrayUtils;
+import libcore.io.IoUtils;
+
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
-import libcore.io.IoUtils;
/**
* Representation of a clipped data on the clipboard.
@@ -665,6 +667,25 @@
b.append("NULL");
}
}
+
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ if (mHtmlText != null) {
+ proto.write(ClipDataProto.Item.HTML_TEXT, mHtmlText);
+ } else if (mText != null) {
+ proto.write(ClipDataProto.Item.TEXT, mText.toString());
+ } else if (mUri != null) {
+ proto.write(ClipDataProto.Item.URI, mUri.toString());
+ } else if (mIntent != null) {
+ mIntent.writeToProto(proto, ClipDataProto.Item.INTENT, true, true, true, true);
+ } else {
+ proto.write(ClipDataProto.Item.NOTHING, true);
+ }
+
+ proto.end(token);
+ }
}
/**
@@ -1048,6 +1069,26 @@
}
/** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ if (mClipDescription != null) {
+ mClipDescription.writeToProto(proto, ClipDataProto.DESCRIPTION);
+ }
+ if (mIcon != null) {
+ final long iToken = proto.start(ClipDataProto.ICON);
+ proto.write(ClipDataProto.Icon.WIDTH, mIcon.getWidth());
+ proto.write(ClipDataProto.Icon.HEIGHT, mIcon.getHeight());
+ proto.end(iToken);
+ }
+ for (int i = 0; i < mItems.size(); i++) {
+ mItems.get(i).writeToProto(proto, ClipDataProto.ITEMS);
+ }
+
+ proto.end(token);
+ }
+
+ /** @hide */
public void collectUris(List<Uri> out) {
for (int i = 0; i < mItems.size(); ++i) {
ClipData.Item item = getItemAt(i);
diff --git a/android/content/ClipDescription.java b/android/content/ClipDescription.java
index 8e30fd6..19295fc 100644
--- a/android/content/ClipDescription.java
+++ b/android/content/ClipDescription.java
@@ -21,6 +21,7 @@
import android.os.PersistableBundle;
import android.text.TextUtils;
import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
@@ -337,6 +338,28 @@
return !first;
}
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ final int size = mMimeTypes.size();
+ for (int i = 0; i < size; i++) {
+ proto.write(ClipDescriptionProto.MIME_TYPES, mMimeTypes.get(i));
+ }
+
+ if (mLabel != null) {
+ proto.write(ClipDescriptionProto.LABEL, mLabel.toString());
+ }
+ if (mExtras != null) {
+ mExtras.writeToProto(proto, ClipDescriptionProto.EXTRAS);
+ }
+ if (mTimeStamp > 0) {
+ proto.write(ClipDescriptionProto.TIMESTAMP_MS, mTimeStamp);
+ }
+
+ proto.end(token);
+ }
+
@Override
public int describeContents() {
return 0;
diff --git a/android/content/ComponentName.java b/android/content/ComponentName.java
index 0d36bdd..ead6c25 100644
--- a/android/content/ComponentName.java
+++ b/android/content/ComponentName.java
@@ -284,9 +284,11 @@
}
/** Put this here so that individual services don't have to reimplement this. @hide */
- public void toProto(ProtoOutputStream proto) {
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
proto.write(ComponentNameProto.PACKAGE_NAME, mPackage);
proto.write(ComponentNameProto.CLASS_NAME, mClass);
+ proto.end(token);
}
@Override
diff --git a/android/content/Context.java b/android/content/Context.java
index 4cedeaa..1b05033 100644
--- a/android/content/Context.java
+++ b/android/content/Context.java
@@ -2851,10 +2851,12 @@
* {@link #BIND_NOT_FOREGROUND}, {@link #BIND_ABOVE_CLIENT},
* {@link #BIND_ALLOW_OOM_MANAGEMENT}, or
* {@link #BIND_WAIVE_PRIORITY}.
- * @return If you have successfully bound to the service, {@code true} is returned;
- * {@code false} is returned if the connection is not made so you will not
- * receive the service object. You should still call {@link #unbindService}
- * to release the connection even if this method returned {@code false}.
+ * @return {@code true} if the system is in the process of bringing up a
+ * service that your client has permission to bind to; {@code false}
+ * if the system couldn't find the service or if your client doesn't
+ * have permission to bind to it. If this value is {@code true}, you
+ * should later call {@link #unbindService} to release the
+ * connection.
*
* @throws SecurityException If the caller does not have permission to access the service
* or the service can not be found.
@@ -3022,7 +3024,8 @@
//@hide: INCIDENT_SERVICE,
//@hide: STATS_COMPANION_SERVICE,
COMPANION_DEVICE_SERVICE,
- CROSS_PROFILE_APPS_SERVICE
+ CROSS_PROFILE_APPS_SERVICE,
+ //@hide: SYSTEM_UPDATE_SERVICE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ServiceName {}
@@ -3221,7 +3224,7 @@
public abstract @Nullable String getSystemServiceName(@NonNull Class<?> serviceClass);
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.PowerManager} for controlling power management,
* including "wake locks," which let you keep the device on while
* you're running long tasks.
@@ -3229,117 +3232,128 @@
public static final String POWER_SERVICE = "power";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.RecoverySystem} for accessing the recovery system
* service.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @hide
*/
public static final String RECOVERY_SERVICE = "recovery";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
+ * {@link android.os.SystemUpdateManager} for accessing the system update
+ * manager service.
+ *
+ * @see #getSystemService(String)
+ * @hide
+ */
+ @SystemApi
+ public static final String SYSTEM_UPDATE_SERVICE = "system_update";
+
+ /**
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.view.WindowManager} for accessing the system's window
* manager.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.view.WindowManager
*/
public static final String WINDOW_SERVICE = "window";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.view.LayoutInflater} for inflating layout resources in this
* context.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.view.LayoutInflater
*/
public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.accounts.AccountManager} for receiving intents at a
* time of your choosing.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.accounts.AccountManager
*/
public static final String ACCOUNT_SERVICE = "account";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.ActivityManager} for interacting with the global
* system state.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.ActivityManager
*/
public static final String ACTIVITY_SERVICE = "activity";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.AlarmManager} for receiving intents at a
* time of your choosing.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.AlarmManager
*/
public static final String ALARM_SERVICE = "alarm";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.NotificationManager} for informing the user of
* background events.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.NotificationManager
*/
public static final String NOTIFICATION_SERVICE = "notification";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.view.accessibility.AccessibilityManager} for giving the user
* feedback for UI events through the registered event listeners.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.view.accessibility.AccessibilityManager
*/
public static final String ACCESSIBILITY_SERVICE = "accessibility";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.view.accessibility.CaptioningManager} for obtaining
* captioning properties and listening for changes in captioning
* preferences.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.view.accessibility.CaptioningManager
*/
public static final String CAPTIONING_SERVICE = "captioning";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.NotificationManager} for controlling keyguard.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.KeyguardManager
*/
public static final String KEYGUARD_SERVICE = "keyguard";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.location.LocationManager} for controlling location
* updates.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.location.LocationManager
*/
public static final String LOCATION_SERVICE = "location";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.location.CountryDetector} for detecting the country that
* the user is in.
*
@@ -3348,96 +3362,96 @@
public static final String COUNTRY_DETECTOR = "country_detector";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.app.SearchManager} for handling searches.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.SearchManager
*/
public static final String SEARCH_SERVICE = "search";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.hardware.SensorManager} for accessing sensors.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.SensorManager
*/
public static final String SENSOR_SERVICE = "sensor";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.os.storage.StorageManager} for accessing system storage
* functions.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.os.storage.StorageManager
*/
public static final String STORAGE_SERVICE = "storage";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.app.usage.StorageStatsManager} for accessing system storage
* statistics.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.usage.StorageStatsManager
*/
public static final String STORAGE_STATS_SERVICE = "storagestats";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* com.android.server.WallpaperService for accessing wallpapers.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String WALLPAPER_SERVICE = "wallpaper";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.os.Vibrator} for interacting with the vibration hardware.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.os.Vibrator
*/
public static final String VIBRATOR_SERVICE = "vibrator";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.app.StatusBarManager} for interacting with the status bar.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.StatusBarManager
* @hide
*/
public static final String STATUS_BAR_SERVICE = "statusbar";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.ConnectivityManager} for handling management of
* network connections.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.ConnectivityManager
*/
public static final String CONNECTIVITY_SERVICE = "connectivity";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.net.IpSecManager} for encrypting Sockets or Networks with
* IPSec.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String IPSEC_SERVICE = "ipsec";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.os.IUpdateLock} for managing runtime sequences that
* must not be interrupted by headless OTA application or similar.
*
* @hide
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.os.UpdateLock
*/
public static final String UPDATE_LOCK_SERVICE = "updatelock";
@@ -3449,18 +3463,18 @@
public static final String NETWORKMANAGEMENT_SERVICE = "network_management";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link com.android.server.slice.SliceManagerService} for managing slices.
* @hide
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String SLICE_SERVICE = "slice";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.app.usage.NetworkStatsManager} for querying network usage stats.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.usage.NetworkStatsManager
*/
public static final String NETWORK_STATS_SERVICE = "netstats";
@@ -3470,40 +3484,40 @@
public static final String NETWORK_WATCHLIST_SERVICE = "network_watchlist";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.wifi.WifiManager} for handling management of
* Wi-Fi access.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.wifi.WifiManager
*/
public static final String WIFI_SERVICE = "wifi";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.wifi.p2p.WifiP2pManager} for handling management of
* Wi-Fi peer-to-peer connections.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.wifi.p2p.WifiP2pManager
*/
public static final String WIFI_P2P_SERVICE = "wifip2p";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.net.wifi.aware.WifiAwareManager} for handling management of
* Wi-Fi Aware.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.wifi.aware.WifiAwareManager
*/
public static final String WIFI_AWARE_SERVICE = "wifiaware";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.wifi.WifiScanner} for scanning the wifi universe
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.wifi.WifiScanner
* @hide
*/
@@ -3511,10 +3525,10 @@
public static final String WIFI_SCANNING_SERVICE = "wifiscanner";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.wifi.RttManager} for ranging devices with wifi
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.wifi.RttManager
* @hide
*/
@@ -3522,24 +3536,23 @@
public static final String WIFI_RTT_SERVICE = "rttmanager";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.wifi.rtt.WifiRttManager} for ranging devices with wifi
*
* Note: this is a replacement for WIFI_RTT_SERVICE above. It will
* be renamed once final implementation in place.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.wifi.rtt.WifiRttManager
- * @hide
*/
- public static final String WIFI_RTT_RANGING_SERVICE = "rttmanager2";
+ public static final String WIFI_RTT_RANGING_SERVICE = "wifirtt";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.lowpan.LowpanManager} for handling management of
* LoWPAN access.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.lowpan.LowpanManager
*
* @hide
@@ -3547,11 +3560,11 @@
public static final String LOWPAN_SERVICE = "lowpan";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.EthernetManager} for handling management of
* Ethernet access.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.EthernetManager
*
* @hide
@@ -3559,98 +3572,98 @@
public static final String ETHERNET_SERVICE = "ethernet";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.nsd.NsdManager} for handling management of network service
* discovery
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.nsd.NsdManager
*/
public static final String NSD_SERVICE = "servicediscovery";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.media.AudioManager} for handling management of volume,
* ringer modes and audio routing.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.media.AudioManager
*/
public static final String AUDIO_SERVICE = "audio";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.hardware.fingerprint.FingerprintManager} for handling management
* of fingerprints.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.fingerprint.FingerprintManager
*/
public static final String FINGERPRINT_SERVICE = "fingerprint";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.media.MediaRouter} for controlling and managing
* routing of media.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.media.MediaRouter
*/
public static final String MEDIA_ROUTER_SERVICE = "media_router";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.media.session.MediaSessionManager} for managing media Sessions.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.media.session.MediaSessionManager
*/
public static final String MEDIA_SESSION_SERVICE = "media_session";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.telephony.TelephonyManager} for handling management the
* telephony features of the device.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.telephony.TelephonyManager
*/
public static final String TELEPHONY_SERVICE = "phone";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.telephony.SubscriptionManager} for handling management the
* telephony subscriptions of the device.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.telephony.SubscriptionManager
*/
public static final String TELEPHONY_SUBSCRIPTION_SERVICE = "telephony_subscription_service";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.telecom.TelecomManager} to manage telecom-related features
* of the device.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.telecom.TelecomManager
*/
public static final String TELECOM_SERVICE = "telecom";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.telephony.CarrierConfigManager} for reading carrier configuration values.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.telephony.CarrierConfigManager
*/
public static final String CARRIER_CONFIG_SERVICE = "carrier_config";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.telephony.euicc.EuiccManager} to manage the device eUICC (embedded SIM).
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.telephony.euicc.EuiccManager
* TODO(b/35851809): Unhide this API.
* @hide
@@ -3658,47 +3671,58 @@
public static final String EUICC_SERVICE = "euicc_service";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
+ * {@link android.telephony.euicc.EuiccCardManager} to access the device eUICC (embedded SIM).
+ *
+ * @see #getSystemService(String)
+ * @see android.telephony.euicc.EuiccCardManager
+ * TODO(b/35851809): Make this a SystemApi.
+ * @hide
+ */
+ public static final String EUICC_CARD_SERVICE = "euicc_card_service";
+
+ /**
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.content.ClipboardManager} for accessing and modifying
* the contents of the global clipboard.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.content.ClipboardManager
*/
public static final String CLIPBOARD_SERVICE = "clipboard";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link TextClassificationManager} for text classification services.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see TextClassificationManager
*/
public static final String TEXT_CLASSIFICATION_SERVICE = "textclassification";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.view.inputmethod.InputMethodManager} for accessing input
* methods.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String INPUT_METHOD_SERVICE = "input_method";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.view.textservice.TextServicesManager} for accessing
* text services.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String TEXT_SERVICES_MANAGER_SERVICE = "textservices";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.appwidget.AppWidgetManager} for accessing AppWidgets.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String APPWIDGET_SERVICE = "appwidget";
@@ -3706,7 +3730,7 @@
* Official published name of the (internal) voice interaction manager service.
*
* @hide
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String VOICE_INTERACTION_MANAGER_SERVICE = "voiceinteraction";
@@ -3714,119 +3738,119 @@
* Official published name of the (internal) autofill service.
*
* @hide
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String AUTOFILL_MANAGER_SERVICE = "autofill";
/**
- * Use with {@link #getSystemService} to access the
+ * Use with {@link #getSystemService(String)} to access the
* {@link com.android.server.voiceinteraction.SoundTriggerService}.
*
* @hide
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String SOUND_TRIGGER_SERVICE = "soundtrigger";
/**
- * Use with {@link #getSystemService} to retrieve an
+ * Use with {@link #getSystemService(String)} to retrieve an
* {@link android.app.backup.IBackupManager IBackupManager} for communicating
* with the backup mechanism.
* @hide
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
@SystemApi
public static final String BACKUP_SERVICE = "backup";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.DropBoxManager} instance for recording
* diagnostic logs.
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String DROPBOX_SERVICE = "dropbox";
/**
* System service name for the DeviceIdleController. There is no Java API for this.
- * @see #getSystemService
+ * @see #getSystemService(String)
* @hide
*/
public static final String DEVICE_IDLE_CONTROLLER = "deviceidle";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.admin.DevicePolicyManager} for working with global
* device policy management.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String DEVICE_POLICY_SERVICE = "device_policy";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.UiModeManager} for controlling UI modes.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String UI_MODE_SERVICE = "uimode";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.DownloadManager} for requesting HTTP downloads.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String DOWNLOAD_SERVICE = "download";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.BatteryManager} for managing battery state.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String BATTERY_SERVICE = "batterymanager";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.nfc.NfcManager} for using NFC.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String NFC_SERVICE = "nfc";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.bluetooth.BluetoothManager} for using Bluetooth.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String BLUETOOTH_SERVICE = "bluetooth";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.net.sip.SipManager} for accessing the SIP related service.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
/** @hide */
public static final String SIP_SERVICE = "sip";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.hardware.usb.UsbManager} for access to USB devices (as a USB host)
* and for controlling this device's behavior as a USB device.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.usb.UsbManager
*/
public static final String USB_SERVICE = "usb";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.hardware.SerialManager} for access to serial ports.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.SerialManager
*
* @hide
@@ -3834,11 +3858,11 @@
public static final String SERIAL_SERVICE = "serial";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.hardware.hdmi.HdmiControlManager} for controlling and managing
* HDMI-CEC protocol.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.hdmi.HdmiControlManager
* @hide
*/
@@ -3846,67 +3870,67 @@
public static final String HDMI_CONTROL_SERVICE = "hdmi_control";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.hardware.input.InputManager} for interacting with input devices.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.input.InputManager
*/
public static final String INPUT_SERVICE = "input";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.hardware.display.DisplayManager} for interacting with display devices.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.display.DisplayManager
*/
public static final String DISPLAY_SERVICE = "display";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.UserManager} for managing users on devices that support multiple users.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.os.UserManager
*/
public static final String USER_SERVICE = "user";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.content.pm.LauncherApps} for querying and monitoring launchable apps across
* profiles of a user.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.content.pm.LauncherApps
*/
public static final String LAUNCHER_APPS_SERVICE = "launcherapps";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.content.RestrictionsManager} for retrieving application restrictions
* and requesting permissions for restricted operations.
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.content.RestrictionsManager
*/
public static final String RESTRICTIONS_SERVICE = "restrictions";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.AppOpsManager} for tracking application operations
* on the device.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.AppOpsManager
*/
public static final String APP_OPS_SERVICE = "appops";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.hardware.camera2.CameraManager} for interacting with
* camera devices.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.camera2.CameraManager
*/
public static final String CAMERA_SERVICE = "camera";
@@ -3915,51 +3939,51 @@
* {@link android.print.PrintManager} for printing and managing
* printers and print tasks.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.print.PrintManager
*/
public static final String PRINT_SERVICE = "print";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.companion.CompanionDeviceManager} for managing companion devices
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.companion.CompanionDeviceManager
*/
public static final String COMPANION_DEVICE_SERVICE = "companiondevice";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.hardware.ConsumerIrManager} for transmitting infrared
* signals from the device.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.ConsumerIrManager
*/
public static final String CONSUMER_IR_SERVICE = "consumer_ir";
/**
* {@link android.app.trust.TrustManager} for managing trust agents.
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.trust.TrustManager
* @hide
*/
public static final String TRUST_SERVICE = "trust";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.media.tv.TvInputManager} for interacting with TV inputs
* on the device.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.media.tv.TvInputManager
*/
public static final String TV_INPUT_SERVICE = "tv_input";
/**
* {@link android.net.NetworkScoreManager} for managing network scoring.
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.NetworkScoreManager
* @hide
*/
@@ -3967,29 +3991,29 @@
public static final String NETWORK_SCORE_SERVICE = "network_score";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.app.usage.UsageStatsManager} for querying device usage stats.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.usage.UsageStatsManager
*/
public static final String USAGE_STATS_SERVICE = "usagestats";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.app.job.JobScheduler} instance for managing occasional
* background tasks.
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.job.JobScheduler
*/
public static final String JOB_SCHEDULER_SERVICE = "jobscheduler";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.service.persistentdata.PersistentDataBlockManager} instance
* for interacting with a storage device that lives across factory resets.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.service.persistentdata.PersistentDataBlockManager
* @hide
*/
@@ -3997,10 +4021,10 @@
public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.service.oemlock.OemLockManager} instance for managing the OEM lock.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.service.oemlock.OemLockManager
* @hide
*/
@@ -4008,54 +4032,54 @@
public static final String OEM_LOCK_SERVICE = "oem_lock";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.media.projection.MediaProjectionManager} instance for managing
* media projection sessions.
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.media.projection.MediaProjectionManager
*/
public static final String MEDIA_PROJECTION_SERVICE = "media_projection";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.media.midi.MidiManager} for accessing the MIDI service.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String MIDI_SERVICE = "midi";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.hardware.radio.RadioManager} for accessing the broadcast radio service.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @hide
*/
public static final String RADIO_SERVICE = "broadcastradio";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.HardwarePropertiesManager} for accessing the hardware properties service.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String HARDWARE_PROPERTIES_SERVICE = "hardware_properties";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.content.pm.ShortcutManager} for accessing the launcher shortcut service.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.content.pm.ShortcutManager
*/
public static final String SHORTCUT_SERVICE = "shortcut";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.hardware.location.ContextHubManager} for accessing context hubs.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.location.ContextHubManager
*
* @hide
@@ -4064,11 +4088,11 @@
public static final String CONTEXTHUB_SERVICE = "contexthub";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.health.SystemHealthManager} for accessing system health (battery, power,
* memory, etc) metrics.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String SYSTEM_HEALTH_SERVICE = "systemhealth";
@@ -4097,46 +4121,46 @@
public static final String STATS_COMPANION_SERVICE = "statscompanion";
/**
- * Use with {@link #getSystemService} to retrieve an {@link android.stats.StatsManager}.
+ * Use with {@link #getSystemService(String)} to retrieve an {@link android.app.StatsManager}.
* @hide
*/
@SystemApi
public static final String STATS_MANAGER = "stats";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.content.om.OverlayManager} for managing overlay packages.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.content.om.OverlayManager
* @hide
*/
public static final String OVERLAY_SERVICE = "overlay";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link VrManager} for accessing the VR service.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @hide
*/
@SystemApi
public static final String VR_SERVICE = "vrmanager";
/**
- * Use with {@link #getSystemService} to retrieve an
+ * Use with {@link #getSystemService(String)} to retrieve an
* {@link android.app.timezone.ITimeZoneRulesManager}.
* @hide
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String TIME_ZONE_RULES_MANAGER_SERVICE = "timezone";
/**
- * Use with {@link #getSystemService} to retrieve a
- * {@link android.content.pm.crossprofile.CrossProfileApps} for cross profile operations.
+ * Use with {@link #getSystemService(String)} to retrieve a
+ * {@link android.content.pm.CrossProfileApps} for cross profile operations.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String CROSS_PROFILE_APPS_SERVICE = "crossprofileapps";
diff --git a/android/content/Intent.java b/android/content/Intent.java
index e940769..acbdf14 100644
--- a/android/content/Intent.java
+++ b/android/content/Intent.java
@@ -3516,7 +3516,10 @@
* For more details see TelephonyIntents.ACTION_SIM_STATE_CHANGED. This is here
* because TelephonyIntents is an internal class.
* @hide
+ * @deprecated Use {@link #ACTION_SIM_CARD_STATE_CHANGED} or
+ * {@link #ACTION_SIM_APPLICATION_STATE_CHANGED}
*/
+ @Deprecated
@SystemApi
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
public static final String ACTION_SIM_STATE_CHANGED = "android.intent.action.SIM_STATE_CHANGED";
@@ -3927,6 +3930,14 @@
@SdkConstant(SdkConstantType.INTENT_CATEGORY)
public static final String CATEGORY_LEANBACK_LAUNCHER = "android.intent.category.LEANBACK_LAUNCHER";
/**
+ * Indicates the preferred entry-point activity when an application is launched from a Car
+ * launcher. If not present, Car launcher can optionally use {@link #CATEGORY_LAUNCHER} as a
+ * fallback, or exclude the application entirely.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String CATEGORY_CAR_LAUNCHER = "android.intent.category.CAR_LAUNCHER";
+ /**
* Indicates a Leanback settings activity to be displayed in the Leanback launcher.
* @hide
*/
@@ -9410,6 +9421,12 @@
}
/** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ // Same input parameters that toString() gives to toShortString().
+ writeToProto(proto, fieldId, true, true, true, false);
+ }
+
+ /** @hide */
public void writeToProto(ProtoOutputStream proto, long fieldId, boolean secure, boolean comp,
boolean extras, boolean clip) {
long token = proto.start(fieldId);
diff --git a/android/content/ServiceConnection.java b/android/content/ServiceConnection.java
index c16dbbe..21398f6 100644
--- a/android/content/ServiceConnection.java
+++ b/android/content/ServiceConnection.java
@@ -31,6 +31,11 @@
* the {@link android.os.IBinder} of the communication channel to the
* Service.
*
+ * <p class="note"><b>Note:</b> If the system has started to bind your
+ * client app to a service, it's possible that your app will never receive
+ * this callback. Your app won't receive a callback if there's an issue with
+ * the service, such as the service crashing while being created.
+ *
* @param name The concrete component name of the service that has
* been connected.
*
diff --git a/android/content/pm/ApplicationInfo.java b/android/content/pm/ApplicationInfo.java
index 15e119b..746a090 100644
--- a/android/content/pm/ApplicationInfo.java
+++ b/android/content/pm/ApplicationInfo.java
@@ -26,7 +26,6 @@
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.os.Environment;
import android.os.Parcel;
import android.os.Parcelable;
@@ -35,6 +34,7 @@
import android.text.TextUtils;
import android.util.Printer;
import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.util.ArrayUtils;
@@ -1184,6 +1184,105 @@
super.dumpBack(pw, prefix);
}
+ /** {@hide} */
+ public void writeToProto(ProtoOutputStream proto, long fieldId, int dumpFlags) {
+ long token = proto.start(fieldId);
+ super.writeToProto(proto, ApplicationInfoProto.PACKAGE);
+ proto.write(ApplicationInfoProto.PERMISSION, permission);
+ proto.write(ApplicationInfoProto.PROCESS_NAME, processName);
+ proto.write(ApplicationInfoProto.UID, uid);
+ proto.write(ApplicationInfoProto.FLAGS, flags);
+ proto.write(ApplicationInfoProto.PRIVATE_FLAGS, privateFlags);
+ proto.write(ApplicationInfoProto.THEME, theme);
+ proto.write(ApplicationInfoProto.SOURCE_DIR, sourceDir);
+ if (!Objects.equals(sourceDir, publicSourceDir)) {
+ proto.write(ApplicationInfoProto.PUBLIC_SOURCE_DIR, publicSourceDir);
+ }
+ if (!ArrayUtils.isEmpty(splitSourceDirs)) {
+ for (String dir : splitSourceDirs) {
+ proto.write(ApplicationInfoProto.SPLIT_SOURCE_DIRS, dir);
+ }
+ }
+ if (!ArrayUtils.isEmpty(splitPublicSourceDirs)
+ && !Arrays.equals(splitSourceDirs, splitPublicSourceDirs)) {
+ for (String dir : splitPublicSourceDirs) {
+ proto.write(ApplicationInfoProto.SPLIT_PUBLIC_SOURCE_DIRS, dir);
+ }
+ }
+ if (resourceDirs != null) {
+ for (String dir : resourceDirs) {
+ proto.write(ApplicationInfoProto.RESOURCE_DIRS, dir);
+ }
+ }
+ proto.write(ApplicationInfoProto.DATA_DIR, dataDir);
+ proto.write(ApplicationInfoProto.CLASS_LOADER_NAME, classLoaderName);
+ if (!ArrayUtils.isEmpty(splitClassLoaderNames)) {
+ for (String name : splitClassLoaderNames) {
+ proto.write(ApplicationInfoProto.SPLIT_CLASS_LOADER_NAMES, name);
+ }
+ }
+
+ long versionToken = proto.start(ApplicationInfoProto.VERSION);
+ proto.write(ApplicationInfoProto.Version.ENABLED, enabled);
+ proto.write(ApplicationInfoProto.Version.MIN_SDK_VERSION, minSdkVersion);
+ proto.write(ApplicationInfoProto.Version.TARGET_SDK_VERSION, targetSdkVersion);
+ proto.write(ApplicationInfoProto.Version.VERSION_CODE, versionCode);
+ proto.write(ApplicationInfoProto.Version.TARGET_SANDBOX_VERSION, targetSandboxVersion);
+ proto.end(versionToken);
+
+ if ((dumpFlags & DUMP_FLAG_DETAILS) != 0) {
+ long detailToken = proto.start(ApplicationInfoProto.DETAIL);
+ if (className != null) {
+ proto.write(ApplicationInfoProto.Detail.CLASS_NAME, className);
+ }
+ proto.write(ApplicationInfoProto.Detail.TASK_AFFINITY, taskAffinity);
+ proto.write(ApplicationInfoProto.Detail.REQUIRES_SMALLEST_WIDTH_DP,
+ requiresSmallestWidthDp);
+ proto.write(ApplicationInfoProto.Detail.COMPATIBLE_WIDTH_LIMIT_DP,
+ compatibleWidthLimitDp);
+ proto.write(ApplicationInfoProto.Detail.LARGEST_WIDTH_LIMIT_DP,
+ largestWidthLimitDp);
+ if (seInfo != null) {
+ proto.write(ApplicationInfoProto.Detail.SEINFO, seInfo);
+ proto.write(ApplicationInfoProto.Detail.SEINFO_USER, seInfoUser);
+ }
+ proto.write(ApplicationInfoProto.Detail.DEVICE_PROTECTED_DATA_DIR,
+ deviceProtectedDataDir);
+ proto.write(ApplicationInfoProto.Detail.CREDENTIAL_PROTECTED_DATA_DIR,
+ credentialProtectedDataDir);
+ if (sharedLibraryFiles != null) {
+ for (String f : sharedLibraryFiles) {
+ proto.write(ApplicationInfoProto.Detail.SHARED_LIBRARY_FILES, f);
+ }
+ }
+ if (manageSpaceActivityName != null) {
+ proto.write(ApplicationInfoProto.Detail.MANAGE_SPACE_ACTIVITY_NAME,
+ manageSpaceActivityName);
+ }
+ if (descriptionRes != 0) {
+ proto.write(ApplicationInfoProto.Detail.DESCRIPTION_RES, descriptionRes);
+ }
+ if (uiOptions != 0) {
+ proto.write(ApplicationInfoProto.Detail.UI_OPTIONS, uiOptions);
+ }
+ proto.write(ApplicationInfoProto.Detail.SUPPORTS_RTL, hasRtlSupport());
+ if (fullBackupContent > 0) {
+ proto.write(ApplicationInfoProto.Detail.CONTENT, "@xml/" + fullBackupContent);
+ } else {
+ proto.write(ApplicationInfoProto.Detail.IS_FULL_BACKUP, fullBackupContent == 0);
+ }
+ if (networkSecurityConfigRes != 0) {
+ proto.write(ApplicationInfoProto.Detail.NETWORK_SECURITY_CONFIG_RES,
+ networkSecurityConfigRes);
+ }
+ if (category != CATEGORY_UNDEFINED) {
+ proto.write(ApplicationInfoProto.Detail.CATEGORY, category);
+ }
+ proto.end(detailToken);
+ }
+ proto.end(token);
+ }
+
/**
* @return true if "supportsRtl" has been set to true in the AndroidManifest
* @hide
@@ -1492,6 +1591,13 @@
/**
* @hide
*/
+ public boolean isAllowedToUseHiddenApi() {
+ return isSystemApp();
+ }
+
+ /**
+ * @hide
+ */
@Override
public Drawable loadDefaultIcon(PackageManager pm) {
if ((flags & FLAG_EXTERNAL_STORAGE) != 0
@@ -1593,11 +1699,6 @@
return (privateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0;
}
- /** @hide */
- public boolean isTargetingDeprecatedSdkVersion() {
- return targetSdkVersion < Build.VERSION.MIN_SUPPORTED_TARGET_SDK_INT;
- }
-
/**
* Returns whether or not this application was installed as a virtual preload.
*/
diff --git a/android/content/pm/crossprofile/CrossProfileApps.java b/android/content/pm/CrossProfileApps.java
similarity index 92%
rename from android/content/pm/crossprofile/CrossProfileApps.java
rename to android/content/pm/CrossProfileApps.java
index 414c138..7d5d609 100644
--- a/android/content/pm/crossprofile/CrossProfileApps.java
+++ b/android/content/pm/CrossProfileApps.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.content.pm.crossprofile;
+package android.content.pm;
import android.annotation.NonNull;
import android.content.ComponentName;
@@ -57,13 +57,14 @@
* action {@link android.content.Intent#ACTION_MAIN}, category
* {@link android.content.Intent#CATEGORY_LAUNCHER}. Otherwise, SecurityException will
* be thrown.
- * @param user The UserHandle of the profile, must be one of the users returned by
+ * @param targetUser The UserHandle of the profile, must be one of the users returned by
* {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will
* be thrown.
*/
- public void startMainActivity(@NonNull ComponentName component, @NonNull UserHandle user) {
+ public void startMainActivity(@NonNull ComponentName component,
+ @NonNull UserHandle targetUser) {
try {
- mService.startActivityAsUser(mContext.getPackageName(), component, user);
+ mService.startActivityAsUser(mContext.getPackageName(), component, targetUser);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
@@ -114,7 +115,7 @@
}
/**
- * Return an icon that calling app can show to user for the semantic of profile switching --
+ * Return a drawable that calling app can show to user for the semantic of profile switching --
* launching its own activity in specified user profile. For example, it may return a briefcase
* icon if the given user handle is the managed profile one.
*
@@ -124,9 +125,9 @@
* @return an icon that calling app can show user for the semantic of launching its own
* activity in specified user profile.
*
- * @see #startMainActivity(ComponentName, UserHandle, Rect, Bundle)
+ * @see #startMainActivity(ComponentName, UserHandle)
*/
- public @NonNull Drawable getProfileSwitchingIcon(@NonNull UserHandle userHandle) {
+ public @NonNull Drawable getProfileSwitchingIconDrawable(@NonNull UserHandle userHandle) {
verifyCanAccessUser(userHandle);
final boolean isManagedProfile =
diff --git a/android/content/pm/OrgApacheHttpLegacyUpdater.java b/android/content/pm/OrgApacheHttpLegacyUpdater.java
new file mode 100644
index 0000000..81041e9
--- /dev/null
+++ b/android/content/pm/OrgApacheHttpLegacyUpdater.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2018 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.content.pm;
+
+import android.content.pm.PackageParser.Package;
+import android.os.Build;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+
+/**
+ * Updates a package to ensure that if it targets < P that the org.apache.http.legacy library is
+ * included by default.
+ *
+ * <p>This is separated out so that it can be conditionally included at build time depending on
+ * whether org.apache.http.legacy is on the bootclasspath or not. In order to include this at
+ * build time, and remove org.apache.http.legacy from the bootclasspath pass
+ * REMOVE_OAHL_FROM_BCP=true on the build command line, otherwise this class will not be included
+ * and the
+ *
+ * @hide
+ */
+@VisibleForTesting
+public class OrgApacheHttpLegacyUpdater extends PackageSharedLibraryUpdater {
+
+ private static final String APACHE_HTTP_LEGACY = "org.apache.http.legacy";
+
+ @Override
+ public void updatePackage(Package pkg) {
+ ArrayList<String> usesLibraries = pkg.usesLibraries;
+ ArrayList<String> usesOptionalLibraries = pkg.usesOptionalLibraries;
+
+ // Packages targeted at <= O_MR1 expect the classes in the org.apache.http.legacy library
+ // to be accessible so this maintains backward compatibility by adding the
+ // org.apache.http.legacy library to those packages.
+ if (apkTargetsApiLevelLessThanOrEqualToOMR1(pkg)) {
+ boolean apacheHttpLegacyPresent = isLibraryPresent(
+ usesLibraries, usesOptionalLibraries, APACHE_HTTP_LEGACY);
+ if (!apacheHttpLegacyPresent) {
+ usesLibraries = prefix(usesLibraries, APACHE_HTTP_LEGACY);
+ }
+ }
+
+ pkg.usesLibraries = usesLibraries;
+ pkg.usesOptionalLibraries = usesOptionalLibraries;
+ }
+
+ private static boolean apkTargetsApiLevelLessThanOrEqualToOMR1(Package pkg) {
+ int targetSdkVersion = pkg.applicationInfo.targetSdkVersion;
+ return targetSdkVersion <= Build.VERSION_CODES.O_MR1;
+ }
+}
diff --git a/android/content/pm/PackageBackwardCompatibility.java b/android/content/pm/PackageBackwardCompatibility.java
index cee2599..9bdb78b 100644
--- a/android/content/pm/PackageBackwardCompatibility.java
+++ b/android/content/pm/PackageBackwardCompatibility.java
@@ -17,12 +17,13 @@
package android.content.pm;
import android.content.pm.PackageParser.Package;
-import android.os.Build;
+import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import java.util.ArrayList;
+import java.util.List;
/**
* Modifies {@link Package} in order to maintain backwards compatibility.
@@ -30,13 +31,60 @@
* @hide
*/
@VisibleForTesting
-public class PackageBackwardCompatibility {
+public class PackageBackwardCompatibility extends PackageSharedLibraryUpdater {
+
+ private static final String TAG = PackageBackwardCompatibility.class.getSimpleName();
private static final String ANDROID_TEST_MOCK = "android.test.mock";
private static final String ANDROID_TEST_RUNNER = "android.test.runner";
- private static final String APACHE_HTTP_LEGACY = "org.apache.http.legacy";
+ private static final PackageBackwardCompatibility INSTANCE;
+
+ static {
+ String className = "android.content.pm.OrgApacheHttpLegacyUpdater";
+ Class<? extends PackageSharedLibraryUpdater> clazz;
+ try {
+ clazz = (PackageBackwardCompatibility.class.getClassLoader()
+ .loadClass(className)
+ .asSubclass(PackageSharedLibraryUpdater.class));
+ } catch (ClassNotFoundException e) {
+ Log.i(TAG, "Could not find " + className + ", ignoring");
+ clazz = null;
+ }
+
+ boolean hasOrgApacheHttpLegacy = false;
+ final List<PackageSharedLibraryUpdater> packageUpdaters = new ArrayList<>();
+ if (clazz == null) {
+ // Add an updater that will remove any references to org.apache.http.library from the
+ // package so that it does not try and load the library when it is on the
+ // bootclasspath.
+ packageUpdaters.add(new RemoveUnnecessaryOrgApacheHttpLegacyLibrary());
+ } else {
+ try {
+ packageUpdaters.add(clazz.getConstructor().newInstance());
+ hasOrgApacheHttpLegacy = true;
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException("Could not create instance of " + className, e);
+ }
+ }
+
+ packageUpdaters.add(new AndroidTestRunnerSplitUpdater());
+
+ PackageSharedLibraryUpdater[] updaterArray = packageUpdaters
+ .toArray(new PackageSharedLibraryUpdater[0]);
+ INSTANCE = new PackageBackwardCompatibility(hasOrgApacheHttpLegacy, updaterArray);
+ }
+
+ private final boolean mRemovedOAHLFromBCP;
+
+ private final PackageSharedLibraryUpdater[] mPackageUpdaters;
+
+ public PackageBackwardCompatibility(boolean removedOAHLFromBCP,
+ PackageSharedLibraryUpdater[] packageUpdaters) {
+ this.mRemovedOAHLFromBCP = removedOAHLFromBCP;
+ this.mPackageUpdaters = packageUpdaters;
+ }
/**
* Modify the shared libraries in the supplied {@link Package} to maintain backwards
@@ -46,44 +94,74 @@
*/
@VisibleForTesting
public static void modifySharedLibraries(Package pkg) {
- ArrayList<String> usesLibraries = pkg.usesLibraries;
- ArrayList<String> usesOptionalLibraries = pkg.usesOptionalLibraries;
+ INSTANCE.updatePackage(pkg);
+ }
- // Packages targeted at <= O_MR1 expect the classes in the org.apache.http.legacy library
- // to be accessible so this maintains backward compatibility by adding the
- // org.apache.http.legacy library to those packages.
- if (apkTargetsApiLevelLessThanOrEqualToOMR1(pkg)) {
- boolean apacheHttpLegacyPresent = isLibraryPresent(
- usesLibraries, usesOptionalLibraries, APACHE_HTTP_LEGACY);
- if (!apacheHttpLegacyPresent) {
- usesLibraries = ArrayUtils.add(usesLibraries, APACHE_HTTP_LEGACY);
+ @Override
+ public void updatePackage(Package pkg) {
+
+ for (PackageSharedLibraryUpdater packageUpdater : mPackageUpdaters) {
+ packageUpdater.updatePackage(pkg);
+ }
+ }
+
+ /**
+ * True if the org.apache.http.legacy has been removed the bootclasspath, false otherwise.
+ */
+ public static boolean removeOAHLFromBCP() {
+ return INSTANCE.mRemovedOAHLFromBCP;
+ }
+
+ /**
+ * Add android.test.mock dependency for any APK that depends on android.test.runner.
+ *
+ * <p>This is needed to maintain backwards compatibility as in previous versions of Android the
+ * android.test.runner library included the classes from android.test.mock which have since
+ * been split out into a separate library.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static class AndroidTestRunnerSplitUpdater extends PackageSharedLibraryUpdater {
+
+ @Override
+ public void updatePackage(Package pkg) {
+ ArrayList<String> usesLibraries = pkg.usesLibraries;
+ ArrayList<String> usesOptionalLibraries = pkg.usesOptionalLibraries;
+
+ // android.test.runner has a dependency on android.test.mock so if android.test.runner
+ // is present but android.test.mock is not then add android.test.mock.
+ boolean androidTestMockPresent = isLibraryPresent(
+ usesLibraries, usesOptionalLibraries, ANDROID_TEST_MOCK);
+ if (ArrayUtils.contains(usesLibraries, ANDROID_TEST_RUNNER)
+ && !androidTestMockPresent) {
+ usesLibraries.add(ANDROID_TEST_MOCK);
}
- }
+ if (ArrayUtils.contains(usesOptionalLibraries, ANDROID_TEST_RUNNER)
+ && !androidTestMockPresent) {
+ usesOptionalLibraries.add(ANDROID_TEST_MOCK);
+ }
- // android.test.runner has a dependency on android.test.mock so if android.test.runner
- // is present but android.test.mock is not then add android.test.mock.
- boolean androidTestMockPresent = isLibraryPresent(
- usesLibraries, usesOptionalLibraries, ANDROID_TEST_MOCK);
- if (ArrayUtils.contains(usesLibraries, ANDROID_TEST_RUNNER) && !androidTestMockPresent) {
- usesLibraries.add(ANDROID_TEST_MOCK);
+ pkg.usesLibraries = usesLibraries;
+ pkg.usesOptionalLibraries = usesOptionalLibraries;
}
- if (ArrayUtils.contains(usesOptionalLibraries, ANDROID_TEST_RUNNER)
- && !androidTestMockPresent) {
- usesOptionalLibraries.add(ANDROID_TEST_MOCK);
- }
-
- pkg.usesLibraries = usesLibraries;
- pkg.usesOptionalLibraries = usesOptionalLibraries;
}
- private static boolean apkTargetsApiLevelLessThanOrEqualToOMR1(Package pkg) {
- int targetSdkVersion = pkg.applicationInfo.targetSdkVersion;
- return targetSdkVersion <= Build.VERSION_CODES.O_MR1;
- }
+ /**
+ * Remove any usages of org.apache.http.legacy from the shared library as the library is on the
+ * bootclasspath.
+ */
+ @VisibleForTesting
+ public static class RemoveUnnecessaryOrgApacheHttpLegacyLibrary
+ extends PackageSharedLibraryUpdater {
- private static boolean isLibraryPresent(ArrayList<String> usesLibraries,
- ArrayList<String> usesOptionalLibraries, String apacheHttpLegacy) {
- return ArrayUtils.contains(usesLibraries, apacheHttpLegacy)
- || ArrayUtils.contains(usesOptionalLibraries, apacheHttpLegacy);
+ private static final String APACHE_HTTP_LEGACY = "org.apache.http.legacy";
+
+ @Override
+ public void updatePackage(Package pkg) {
+ pkg.usesLibraries = ArrayUtils.remove(pkg.usesLibraries, APACHE_HTTP_LEGACY);
+ pkg.usesOptionalLibraries =
+ ArrayUtils.remove(pkg.usesOptionalLibraries, APACHE_HTTP_LEGACY);
+ }
}
}
diff --git a/android/content/pm/PackageInfo.java b/android/content/pm/PackageInfo.java
index 5a91e94..09a46b8 100644
--- a/android/content/pm/PackageInfo.java
+++ b/android/content/pm/PackageInfo.java
@@ -16,14 +16,10 @@
package android.content.pm;
-import android.annotation.IntDef;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
/**
* Overall information about the contents of a package. This corresponds
* to all of the information collected from AndroidManifest.xml.
@@ -246,9 +242,44 @@
* equivalent to being signed with certificates B and A. This means that
* in case multiple signatures are reported you cannot assume the one at
* the first position to be the same across updates.
+ *
+ * <strong>Deprecated</strong> This has been replaced by the
+ * {@link PackageInfo#signingCertificateHistory} field, which takes into
+ * account signing certificate rotation. For backwards compatibility in
+ * the event of signing certificate rotation, this will return the oldest
+ * reported signing certificate, so that an application will appear to
+ * callers as though no rotation occurred.
+ *
+ * @deprecated use {@code signingCertificateHistory} instead
*/
+ @Deprecated
public Signature[] signatures;
-
+
+ /**
+ * Array of all signatures arrays read from the package file, potentially
+ * including past signing certificates no longer used after signing
+ * certificate rotation. Though signing certificate rotation is only
+ * available for apps with a single signing certificate, this provides an
+ * array of arrays so that packages signed with multiple signing
+ * certificates can still return all signers. This is only filled in if
+ * the flag {@link PackageManager#GET_SIGNING_CERTIFICATES} was set.
+ *
+ * A package must be singed with at least one certificate, which is at
+ * position zero in the array. An application may be signed by multiple
+ * certificates, which would be in the array at position zero in an
+ * indeterminate order. A package may also have a history of certificates
+ * due to signing certificate rotation. In this case, the array will be
+ * populated by a series of single-entry arrays corresponding to a signing
+ * certificate of the package.
+ *
+ * <strong>Note:</strong> Signature ordering is not guaranteed to be
+ * stable which means that a package signed with certificates A and B is
+ * equivalent to being signed with certificates B and A. This means that
+ * in case multiple signatures are reported you cannot assume the one at
+ * the first position will be the same across updates.
+ */
+ public Signature[][] signingCertificateHistory;
+
/**
* Application specified preferred configuration
* {@link android.R.styleable#AndroidManifestUsesConfiguration
@@ -335,28 +366,9 @@
public int overlayPriority;
/**
- * Flag for use with {@link #mOverlayFlags}. Marks the overlay as static, meaning it cannot
- * be enabled/disabled at runtime.
+ * Whether the overlay is static, meaning it cannot be enabled/disabled at runtime.
*/
- static final int FLAG_OVERLAY_STATIC = 1 << 1;
-
- /**
- * Flag for use with {@link #mOverlayFlags}. Marks the overlay as trusted (not 3rd party).
- */
- static final int FLAG_OVERLAY_TRUSTED = 1 << 2;
-
- @IntDef(flag = true, prefix = "FLAG_OVERLAY_", value = {
- FLAG_OVERLAY_STATIC,
- FLAG_OVERLAY_TRUSTED
- })
- @Retention(RetentionPolicy.SOURCE)
- @interface OverlayFlags {}
-
- /**
- * Modifiers that affect the state of this overlay. See {@link #FLAG_OVERLAY_STATIC},
- * {@link #FLAG_OVERLAY_TRUSTED}.
- */
- @OverlayFlags int mOverlayFlags;
+ boolean mOverlayIsStatic;
/**
* The user-visible SDK version (ex. 26) of the framework against which the application claims
@@ -389,7 +401,7 @@
* @hide
*/
public boolean isOverlayPackage() {
- return overlayTarget != null && (mOverlayFlags & FLAG_OVERLAY_TRUSTED) != 0;
+ return overlayTarget != null;
}
/**
@@ -398,7 +410,7 @@
* @hide
*/
public boolean isStaticOverlayPackage() {
- return overlayTarget != null && (mOverlayFlags & FLAG_OVERLAY_STATIC) != 0;
+ return overlayTarget != null && mOverlayIsStatic;
}
@Override
@@ -453,7 +465,7 @@
dest.writeString(requiredAccountType);
dest.writeString(overlayTarget);
dest.writeInt(overlayPriority);
- dest.writeInt(mOverlayFlags);
+ dest.writeBoolean(mOverlayIsStatic);
dest.writeInt(compileSdkVersion);
dest.writeString(compileSdkVersionCodename);
}
@@ -508,7 +520,7 @@
requiredAccountType = source.readString();
overlayTarget = source.readString();
overlayPriority = source.readInt();
- mOverlayFlags = source.readInt();
+ mOverlayIsStatic = source.readBoolean();
compileSdkVersion = source.readInt();
compileSdkVersionCodename = source.readString();
diff --git a/android/content/pm/PackageInstaller.java b/android/content/pm/PackageInstaller.java
index 77c5743..df677d2 100644
--- a/android/content/pm/PackageInstaller.java
+++ b/android/content/pm/PackageInstaller.java
@@ -324,7 +324,14 @@
*/
public int createSession(@NonNull SessionParams params) throws IOException {
try {
- return mInstaller.createSession(params, mInstallerPackageName, mUserId);
+ final String installerPackage;
+ if (params.installerPackageName == null) {
+ installerPackage = mInstallerPackageName;
+ } else {
+ installerPackage = params.installerPackageName;
+ }
+
+ return mInstaller.createSession(params, installerPackage, mUserId);
} catch (RuntimeException e) {
ExceptionUtils.maybeUnwrapIOException(e);
throw e;
@@ -1081,6 +1088,8 @@
public String volumeUuid;
/** {@hide} */
public String[] grantedRuntimePermissions;
+ /** {@hide} */
+ public String installerPackageName;
/**
* Construct parameters for a new package install session.
@@ -1109,6 +1118,7 @@
abiOverride = source.readString();
volumeUuid = source.readString();
grantedRuntimePermissions = source.readStringArray();
+ installerPackageName = source.readString();
}
/**
@@ -1304,6 +1314,18 @@
}
}
+ /**
+ * Set the installer package for the app.
+ *
+ * By default this is the app that created the {@link PackageInstaller} object.
+ *
+ * @param installerPackageName name of the installer package
+ * {@hide}
+ */
+ public void setInstallerPackageName(String installerPackageName) {
+ this.installerPackageName = installerPackageName;
+ }
+
/** {@hide} */
public void dump(IndentingPrintWriter pw) {
pw.printPair("mode", mode);
@@ -1319,6 +1341,7 @@
pw.printPair("abiOverride", abiOverride);
pw.printPair("volumeUuid", volumeUuid);
pw.printPair("grantedRuntimePermissions", grantedRuntimePermissions);
+ pw.printPair("installerPackageName", installerPackageName);
pw.println();
}
@@ -1343,6 +1366,7 @@
dest.writeString(abiOverride);
dest.writeString(volumeUuid);
dest.writeStringArray(grantedRuntimePermissions);
+ dest.writeString(installerPackageName);
}
public static final Parcelable.Creator<SessionParams>
diff --git a/android/content/pm/PackageItemInfo.java b/android/content/pm/PackageItemInfo.java
index 11830c2..2c0c6ad 100644
--- a/android/content/pm/PackageItemInfo.java
+++ b/android/content/pm/PackageItemInfo.java
@@ -19,7 +19,6 @@
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.content.res.XmlResourceParser;
-
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Parcel;
@@ -28,6 +27,8 @@
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.Printer;
+import android.util.proto.ProtoOutputStream;
+
import java.text.Collator;
import java.util.Comparator;
@@ -386,6 +387,24 @@
dest.writeInt(showUserIcon);
}
+ /**
+ * @hide
+ */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ long token = proto.start(fieldId);
+ if (name != null) {
+ proto.write(PackageItemInfoProto.NAME, name);
+ }
+ proto.write(PackageItemInfoProto.PACKAGE_NAME, packageName);
+ if (labelRes != 0 || nonLocalizedLabel != null || icon != 0 || banner != 0) {
+ proto.write(PackageItemInfoProto.LABEL_RES, labelRes);
+ proto.write(PackageItemInfoProto.NON_LOCALIZED_LABEL, nonLocalizedLabel.toString());
+ proto.write(PackageItemInfoProto.ICON, icon);
+ proto.write(PackageItemInfoProto.BANNER, banner);
+ }
+ proto.end(token);
+ }
+
protected PackageItemInfo(Parcel source) {
name = source.readString();
packageName = source.readString();
diff --git a/android/content/pm/PackageManager.java b/android/content/pm/PackageManager.java
index 2d72632..df69d80 100644
--- a/android/content/pm/PackageManager.java
+++ b/android/content/pm/PackageManager.java
@@ -47,7 +47,6 @@
import android.content.res.XmlResourceParser;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
-import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Bundle;
@@ -134,6 +133,7 @@
GET_SERVICES,
GET_SHARED_LIBRARY_FILES,
GET_SIGNATURES,
+ GET_SIGNING_CERTIFICATES,
GET_URI_PERMISSION_PATTERNS,
MATCH_UNINSTALLED_PACKAGES,
MATCH_DISABLED_COMPONENTS,
@@ -273,7 +273,10 @@
/**
* {@link PackageInfo} flag: return information about the
* signatures included in the package.
+ *
+ * @deprecated use {@code GET_SIGNING_CERTIFICATES} instead
*/
+ @Deprecated
public static final int GET_SIGNATURES = 0x00000040;
/**
@@ -489,6 +492,14 @@
public static final int MATCH_STATIC_SHARED_LIBRARIES = 0x04000000;
/**
+ * {@link PackageInfo} flag: return the signing certificates associated with
+ * this package. Each entry is a signing certificate that the package
+ * has proven it is authorized to use, usually a past signing certificate from
+ * which it has rotated.
+ */
+ public static final int GET_SIGNING_CERTIFICATES = 0x08000000;
+
+ /**
* Internal flag used to indicate that a system component has done their
* homework and verified that they correctly handle packages and components
* that come and go over time. In particular:
@@ -788,7 +799,8 @@
/**
* Flag parameter for {@link #installPackage} to indicate that this package is an
- * upgrade to a package that refers to the SDK via release letter.
+ * upgrade to a package that refers to the SDK via release letter or is targeting an SDK via
+ * release letter that the current build does not support.
*
* @hide
*/
@@ -1297,6 +1309,15 @@
*/
public static final int INSTALL_FAILED_INSTANT_APP_INVALID = -116;
+ /**
+ * Installation parse return code: this is passed in the
+ * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the dex metadata file is invalid or
+ * if there was no matching apk file for a dex metadata file.
+ *
+ * @hide
+ */
+ public static final int INSTALL_FAILED_BAD_DEX_METADATA = -117;
+
/** @hide */
@IntDef(flag = true, prefix = { "DELETE_" }, value = {
DELETE_KEEP_DATA,
@@ -1745,6 +1766,16 @@
"android.hardware.camera.capability.raw";
/**
+ * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: At least one
+ * of the cameras on the device supports the
+ * {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING
+ * MOTION_TRACKING} capability level.
+ */
+ @SdkConstant(SdkConstantType.FEATURE)
+ public static final String FEATURE_CAMERA_AR =
+ "android.hardware.camera.ar";
+
+ /**
* Feature for {@link #getSystemAvailableFeatures} and
* {@link #hasSystemFeature}: The device is capable of communicating with
* consumer IR devices.
@@ -2328,8 +2359,6 @@
/**
* Feature for {@link #getSystemAvailableFeatures} and
* {@link #hasSystemFeature}: The device supports Wi-Fi RTT (IEEE 802.11mc).
- *
- * @hide RTT_API
*/
@SdkConstant(SdkConstantType.FEATURE)
public static final String FEATURE_WIFI_RTT = "android.hardware.wifi.rtt";
@@ -2480,10 +2509,17 @@
= "android.software.securely_removes_users";
/** {@hide} */
+ @TestApi
@SdkConstant(SdkConstantType.FEATURE)
public static final String FEATURE_FILE_BASED_ENCRYPTION
= "android.software.file_based_encryption";
+ /** {@hide} */
+ @TestApi
+ @SdkConstant(SdkConstantType.FEATURE)
+ public static final String FEATURE_ADOPTABLE_STORAGE
+ = "android.software.adoptable_storage";
+
/**
* Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:
* The device has a full implementation of the android.webkit.* APIs. Devices
@@ -2530,31 +2566,22 @@
* Devices declaring this feature must include an application implementing a
* {@link android.service.vr.VrListenerService} that can be targeted by VR applications via
* {@link android.app.Activity#setVrModeEnabled}.
+ * @deprecated use {@link #FEATURE_VR_MODE_HIGH_PERFORMANCE} instead.
*/
+ @Deprecated
@SdkConstant(SdkConstantType.FEATURE)
public static final String FEATURE_VR_MODE = "android.software.vr.mode";
/**
* Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:
- * The device implements {@link #FEATURE_VR_MODE} but additionally meets extra CDD requirements
- * to provide a high-quality VR experience. In general, devices declaring this feature will
- * additionally:
- * <ul>
- * <li>Deliver consistent performance at a high framerate over an extended period of time
- * for typical VR application CPU/GPU workloads with a minimal number of frame drops for VR
- * applications that have called
- * {@link android.view.Window#setSustainedPerformanceMode}.</li>
- * <li>Implement {@link #FEATURE_HIFI_SENSORS} and have a low sensor latency.</li>
- * <li>Include optimizations to lower display persistence while running VR applications.</li>
- * <li>Implement an optimized render path to minimize latency to draw to the device's main
- * display.</li>
- * <li>Include the following EGL extensions: EGL_ANDROID_create_native_client_buffer,
- * EGL_ANDROID_front_buffer_auto_refresh, EGL_EXT_protected_content,
- * EGL_KHR_mutable_render_buffer, EGL_KHR_reusable_sync, and EGL_KHR_wait_sync.</li>
- * <li>Provide at least one CPU core that is reserved for use solely by the top, foreground
- * VR application process for critical render threads while such an application is
- * running.</li>
- * </ul>
+ * The device implements an optimized mode for virtual reality (VR) applications that handles
+ * stereoscopic rendering of notifications, disables most monocular system UI components
+ * while a VR application has user focus and meets extra CDD requirements to provide a
+ * high-quality VR experience.
+ * Devices declaring this feature must include an application implementing a
+ * {@link android.service.vr.VrListenerService} that can be targeted by VR applications via
+ * {@link android.app.Activity#setVrModeEnabled}.
+ * and must meet CDD requirements to provide a high-quality VR experience.
*/
@SdkConstant(SdkConstantType.FEATURE)
public static final String FEATURE_VR_MODE_HIGH_PERFORMANCE
@@ -3039,6 +3066,21 @@
public abstract @Nullable Intent getLeanbackLaunchIntentForPackage(@NonNull String packageName);
/**
+ * Return a "good" intent to launch a front-door Car activity in a
+ * package, for use for example to implement an "open" button when browsing
+ * through packages. The current implementation will look for a main
+ * activity in the category {@link Intent#CATEGORY_CAR_LAUNCHER}, or
+ * return null if no main car activities are found.
+ *
+ * @param packageName The name of the package to inspect.
+ * @return Returns either a fully-qualified Intent that can be used to launch
+ * the main Car activity in the package, or null if the package
+ * does not contain such an activity.
+ * @hide
+ */
+ public abstract @Nullable Intent getCarLaunchIntentForPackage(@NonNull String packageName);
+
+ /**
* Return an array of all of the POSIX secondary group IDs that have been
* assigned to the given package.
* <p>
@@ -3761,7 +3803,7 @@
public abstract int getInstantAppCookieMaxBytes();
/**
- * @deprecated
+ * deprecated
* @hide
*/
public abstract int getInstantAppCookieMaxSize();
@@ -4718,17 +4760,6 @@
}
/**
- * @deprecated replaced by {@link PackageInstaller}
- * @hide
- */
- @Deprecated
- public abstract void installPackage(
- Uri packageURI,
- PackageInstallObserver observer,
- @InstallFlags int flags,
- String installerPackageName);
-
- /**
* If there is already an application with the given package name installed
* on the system for other users, also install it for the calling user.
* @hide
@@ -5633,6 +5664,8 @@
case INSTALL_FAILED_DUPLICATE_PERMISSION: return "INSTALL_FAILED_DUPLICATE_PERMISSION";
case INSTALL_FAILED_NO_MATCHING_ABIS: return "INSTALL_FAILED_NO_MATCHING_ABIS";
case INSTALL_FAILED_ABORTED: return "INSTALL_FAILED_ABORTED";
+ case INSTALL_FAILED_BAD_DEX_METADATA:
+ return "INSTALL_FAILED_BAD_DEX_METADATA";
default: return Integer.toString(status);
}
}
@@ -5677,6 +5710,7 @@
case INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID: return PackageInstaller.STATUS_FAILURE_INVALID;
case INSTALL_PARSE_FAILED_MANIFEST_MALFORMED: return PackageInstaller.STATUS_FAILURE_INVALID;
case INSTALL_PARSE_FAILED_MANIFEST_EMPTY: return PackageInstaller.STATUS_FAILURE_INVALID;
+ case INSTALL_FAILED_BAD_DEX_METADATA: return PackageInstaller.STATUS_FAILURE_INVALID;
case INSTALL_FAILED_INTERNAL_ERROR: return PackageInstaller.STATUS_FAILURE;
case INSTALL_FAILED_USER_RESTRICTED: return PackageInstaller.STATUS_FAILURE_INCOMPATIBLE;
case INSTALL_FAILED_DUPLICATE_PERMISSION: return PackageInstaller.STATUS_FAILURE_CONFLICT;
@@ -5869,4 +5903,93 @@
public @NonNull ArtManager getArtManager() {
throw new UnsupportedOperationException("getArtManager not implemented in subclass");
}
+
+ /**
+ * Sets or clears the harmful app warning details for the given app.
+ *
+ * When set, any attempt to launch an activity in this package will be intercepted and a
+ * warning dialog will be shown to the user instead, with the given warning. The user
+ * will have the option to proceed with the activity launch, or to uninstall the application.
+ *
+ * @param packageName The full name of the package to warn on.
+ * @param warning A warning string to display to the user describing the threat posed by the
+ * application, or null to clear the warning.
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.SET_HARMFUL_APP_WARNINGS)
+ @SystemApi
+ public void setHarmfulAppWarning(@NonNull String packageName, @Nullable CharSequence warning) {
+ throw new UnsupportedOperationException("setHarmfulAppWarning not implemented in subclass");
+ }
+
+ /**
+ * Returns the harmful app warning string for the given app, or null if there is none set.
+ *
+ * @param packageName The full name of the desired package.
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.SET_HARMFUL_APP_WARNINGS)
+ @Nullable
+ @SystemApi
+ public CharSequence getHarmfulAppWarning(@NonNull String packageName) {
+ throw new UnsupportedOperationException("getHarmfulAppWarning not implemented in subclass");
+ }
+
+ /** @hide */
+ @IntDef(prefix = { "CERT_INPUT_" }, value = {
+ CERT_INPUT_RAW_X509,
+ CERT_INPUT_SHA256
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CertificateInputType {}
+
+ /**
+ * Certificate input bytes: the input bytes represent an encoded X.509 Certificate which could
+ * be generated using an {@code CertificateFactory}
+ */
+ public static final int CERT_INPUT_RAW_X509 = 0;
+
+ /**
+ * Certificate input bytes: the input bytes represent the SHA256 output of an encoded X.509
+ * Certificate.
+ */
+ public static final int CERT_INPUT_SHA256 = 1;
+
+ /**
+ * Searches the set of signing certificates by which the given package has proven to have been
+ * signed. This should be used instead of {@code getPackageInfo} with {@code GET_SIGNATURES}
+ * since it takes into account the possibility of signing certificate rotation, except in the
+ * case of packages that are signed by multiple certificates, for which signing certificate
+ * rotation is not supported.
+ *
+ * @param packageName package whose signing certificates to check
+ * @param certificate signing certificate for which to search
+ * @param type representation of the {@code certificate}
+ * @return true if this package was or is signed by exactly the certificate {@code certificate}
+ */
+ public boolean hasSigningCertificate(
+ String packageName, byte[] certificate, @CertificateInputType int type) {
+ throw new UnsupportedOperationException(
+ "hasSigningCertificate not implemented in subclass");
+ }
+
+ /**
+ * Searches the set of signing certificates by which the given uid has proven to have been
+ * signed. This should be used instead of {@code getPackageInfo} with {@code GET_SIGNATURES}
+ * since it takes into account the possibility of signing certificate rotation, except in the
+ * case of packages that are signed by multiple certificates, for which signing certificate
+ * rotation is not supported.
+ *
+ * @param uid package whose signing certificates to check
+ * @param certificate signing certificate for which to search
+ * @param type representation of the {@code certificate}
+ * @return true if this package was or is signed by exactly the certificate {@code certificate}
+ */
+ public boolean hasSigningCertificate(
+ int uid, byte[] certificate, @CertificateInputType int type) {
+ throw new UnsupportedOperationException(
+ "hasSigningCertificate not implemented in subclass");
+ }
}
diff --git a/android/content/pm/PackageManagerInternal.java b/android/content/pm/PackageManagerInternal.java
index 8ee8e10..6f093ba 100644
--- a/android/content/pm/PackageManagerInternal.java
+++ b/android/content/pm/PackageManagerInternal.java
@@ -119,6 +119,12 @@
public abstract void setSimCallManagerPackagesProvider(PackagesProvider provider);
/**
+ * Sets the Use Open Wifi packages provider.
+ * @param provider The packages provider.
+ */
+ public abstract void setUseOpenWifiAppPackagesProvider(PackagesProvider provider);
+
+ /**
* Sets the sync adapter packages provider.
* @param provider The provider.
*/
@@ -147,6 +153,14 @@
int userId);
/**
+ * Requests granting of the default permissions to the current default Use Open Wifi app.
+ * @param packageName The default use open wifi package name.
+ * @param userId The user for which to grant the permissions.
+ */
+ public abstract void grantDefaultPermissionsToDefaultUseOpenWifiApp(String packageName,
+ int userId);
+
+ /**
* Sets a list of apps to keep in PM's internal data structures and as APKs even if no user has
* currently installed it. The apps are not preloaded.
* @param packageList List of package names to keep cached.
@@ -422,6 +436,11 @@
*/
public abstract int getUidTargetSdkVersion(int uid);
+ /**
+ * Return the taget SDK version for the app with the given package name.
+ */
+ public abstract int getPackageTargetSdkVersion(String packageName);
+
/** Whether the binder caller can access instant apps. */
public abstract boolean canAccessInstantApps(int callingUid, int userId);
diff --git a/android/content/pm/PackageParser.java b/android/content/pm/PackageParser.java
index 77eb57f..24e3dfa 100644
--- a/android/content/pm/PackageParser.java
+++ b/android/content/pm/PackageParser.java
@@ -35,7 +35,6 @@
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NOT_APK;
-import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
@@ -85,7 +84,6 @@
import android.util.Slog;
import android.util.SparseArray;
import android.util.TypedValue;
-import android.util.apk.ApkSignatureSchemeV2Verifier;
import android.util.apk.ApkSignatureVerifier;
import android.view.Gravity;
@@ -112,8 +110,7 @@
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
@@ -158,10 +155,6 @@
private static final boolean MULTI_PACKAGE_APK_ENABLED = Build.IS_DEBUGGABLE &&
SystemProperties.getBoolean(PROPERTY_CHILD_PACKAGES_ENABLED, false);
- public static final int APK_SIGNING_UNKNOWN = 0;
- public static final int APK_SIGNING_V1 = 1;
- public static final int APK_SIGNING_V2 = 2;
-
private static final float DEFAULT_PRE_O_MAX_ASPECT_RATIO = 1.86f;
// TODO: switch outError users to PackageParserException
@@ -247,6 +240,9 @@
}
/** @hide */
+ public static final String APK_FILE_EXTENSION = ".apk";
+
+ /** @hide */
public static class NewPermissionInfo {
public final String name;
public final int sdkVersion;
@@ -477,8 +473,7 @@
public final int revisionCode;
public final int installLocation;
public final VerifierInfo[] verifiers;
- public final Signature[] signatures;
- public final Certificate[][] certificates;
+ public final SigningDetails signingDetails;
public final boolean coreApp;
public final boolean debuggable;
public final boolean multiArch;
@@ -486,10 +481,11 @@
public final boolean extractNativeLibs;
public final boolean isolatedSplits;
- public ApkLite(String codePath, String packageName, String splitName, boolean isFeatureSplit,
+ public ApkLite(String codePath, String packageName, String splitName,
+ boolean isFeatureSplit,
String configForSplit, String usesSplitName, int versionCode, int versionCodeMajor,
int revisionCode, int installLocation, List<VerifierInfo> verifiers,
- Signature[] signatures, Certificate[][] certificates, boolean coreApp,
+ SigningDetails signingDetails, boolean coreApp,
boolean debuggable, boolean multiArch, boolean use32bitAbi,
boolean extractNativeLibs, boolean isolatedSplits) {
this.codePath = codePath;
@@ -502,9 +498,8 @@
this.versionCodeMajor = versionCodeMajor;
this.revisionCode = revisionCode;
this.installLocation = installLocation;
+ this.signingDetails = signingDetails;
this.verifiers = verifiers.toArray(new VerifierInfo[verifiers.size()]);
- this.signatures = signatures;
- this.certificates = certificates;
this.coreApp = coreApp;
this.debuggable = debuggable;
this.multiArch = multiArch;
@@ -621,7 +616,7 @@
}
public static boolean isApkPath(String path) {
- return path.endsWith(".apk");
+ return path.endsWith(APK_FILE_EXTENSION);
}
/**
@@ -684,15 +679,7 @@
pi.requiredAccountType = p.mRequiredAccountType;
pi.overlayTarget = p.mOverlayTarget;
pi.overlayPriority = p.mOverlayPriority;
-
- if (p.mIsStaticOverlay) {
- pi.mOverlayFlags |= PackageInfo.FLAG_OVERLAY_STATIC;
- }
-
- if (p.mTrustedOverlay) {
- pi.mOverlayFlags |= PackageInfo.FLAG_OVERLAY_TRUSTED;
- }
-
+ pi.mOverlayIsStatic = p.mOverlayIsStatic;
pi.compileSdkVersion = p.mCompileSdkVersion;
pi.compileSdkVersionCodename = p.mCompileSdkVersionCodename;
pi.firstInstallTime = firstInstallTime;
@@ -806,11 +793,38 @@
}
}
}
+ // deprecated method of getting signing certificates
if ((flags&PackageManager.GET_SIGNATURES) != 0) {
- int N = (p.mSignatures != null) ? p.mSignatures.length : 0;
- if (N > 0) {
- pi.signatures = new Signature[N];
- System.arraycopy(p.mSignatures, 0, pi.signatures, 0, N);
+ if (p.mSigningDetails.hasPastSigningCertificates()) {
+ // Package has included signing certificate rotation information. Return the oldest
+ // cert so that programmatic checks keep working even if unaware of key rotation.
+ pi.signatures = new Signature[1];
+ pi.signatures[0] = p.mSigningDetails.pastSigningCertificates[0];
+ } else if (p.mSigningDetails.hasSignatures()) {
+ // otherwise keep old behavior
+ int numberOfSigs = p.mSigningDetails.signatures.length;
+ pi.signatures = new Signature[numberOfSigs];
+ System.arraycopy(p.mSigningDetails.signatures, 0, pi.signatures, 0, numberOfSigs);
+ }
+ }
+
+ // replacement for GET_SIGNATURES
+ if ((flags & PackageManager.GET_SIGNING_CERTIFICATES) != 0) {
+ if (p.mSigningDetails.hasPastSigningCertificates()) {
+ // Package has included signing certificate rotation information. Convert each
+ // entry to an array
+ int numberOfSigs = p.mSigningDetails.pastSigningCertificates.length;
+ pi.signingCertificateHistory = new Signature[numberOfSigs][];
+ for (int i = 0; i < numberOfSigs; i++) {
+ pi.signingCertificateHistory[i] =
+ new Signature[] { p.mSigningDetails.pastSigningCertificates[i] };
+ }
+ } else if (p.mSigningDetails.hasSignatures()) {
+ // otherwise keep old behavior
+ int numberOfSigs = p.mSigningDetails.signatures.length;
+ pi.signingCertificateHistory = new Signature[1][numberOfSigs];
+ System.arraycopy(p.mSigningDetails.signatures, 0,
+ pi.signingCertificateHistory[0], 0, numberOfSigs);
}
}
return pi;
@@ -818,15 +832,14 @@
public static final int PARSE_MUST_BE_APK = 1 << 0;
public static final int PARSE_IGNORE_PROCESSES = 1 << 1;
+ /** @deprecated forward lock no longer functional. remove. */
+ @Deprecated
public static final int PARSE_FORWARD_LOCK = 1 << 2;
public static final int PARSE_EXTERNAL_STORAGE = 1 << 3;
public static final int PARSE_IS_SYSTEM_DIR = 1 << 4;
public static final int PARSE_COLLECT_CERTIFICATES = 1 << 5;
public static final int PARSE_ENFORCE_CODE = 1 << 6;
public static final int PARSE_FORCE_SDK = 1 << 7;
- /** @deprecated remove when fixing b/68860689 */
- @Deprecated
- public static final int PARSE_IS_EPHEMERAL = 1 << 8;
public static final int PARSE_CHATTY = 1 << 31;
@IntDef(flag = true, prefix = { "PARSE_" }, value = {
@@ -837,7 +850,6 @@
PARSE_FORCE_SDK,
PARSE_FORWARD_LOCK,
PARSE_IGNORE_PROCESSES,
- PARSE_IS_EPHEMERAL,
PARSE_IS_SYSTEM_DIR,
PARSE_MUST_BE_APK,
})
@@ -1349,7 +1361,7 @@
pkg.setVolumeUuid(volumeUuid);
pkg.setApplicationVolumeUuid(volumeUuid);
pkg.setBaseCodePath(apkPath);
- pkg.setSignatures(null);
+ pkg.setSigningDetails(SigningDetails.UNKNOWN);
return pkg;
@@ -1469,57 +1481,19 @@
return pkg;
}
- public static int getApkSigningVersion(Package pkg) {
- try {
- if (ApkSignatureSchemeV2Verifier.hasSignature(pkg.baseCodePath)) {
- return APK_SIGNING_V2;
- }
- return APK_SIGNING_V1;
- } catch (IOException e) {
+ /** Parses the public keys from the set of signatures. */
+ public static ArraySet<PublicKey> toSigningKeys(Signature[] signatures)
+ throws CertificateException {
+ ArraySet<PublicKey> keys = new ArraySet<>(signatures.length);
+ for (int i = 0; i < signatures.length; i++) {
+ keys.add(signatures[i].getPublicKey());
}
- return APK_SIGNING_UNKNOWN;
- }
-
- /**
- * Populates the correct packages fields with the given certificates.
- * <p>
- * This is useful when we've already processed the certificates [such as during package
- * installation through an installer session]. We don't re-process the archive and
- * simply populate the correct fields.
- */
- public static void populateCertificates(Package pkg, Certificate[][] certificates)
- throws PackageParserException {
- pkg.mCertificates = null;
- pkg.mSignatures = null;
- pkg.mSigningKeys = null;
-
- pkg.mCertificates = certificates;
- try {
- pkg.mSignatures = ApkSignatureVerifier.convertToSignatures(certificates);
- } catch (CertificateEncodingException e) {
- // certificates weren't encoded properly; something went wrong
- throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
- "Failed to collect certificates from " + pkg.baseCodePath, e);
- }
- pkg.mSigningKeys = new ArraySet<>(certificates.length);
- for (int i = 0; i < certificates.length; i++) {
- Certificate[] signerCerts = certificates[i];
- Certificate signerCert = signerCerts[0];
- pkg.mSigningKeys.add(signerCert.getPublicKey());
- }
- // add signatures to child packages
- final int childCount = (pkg.childPackages != null) ? pkg.childPackages.size() : 0;
- for (int i = 0; i < childCount; i++) {
- Package childPkg = pkg.childPackages.get(i);
- childPkg.mCertificates = pkg.mCertificates;
- childPkg.mSignatures = pkg.mSignatures;
- childPkg.mSigningKeys = pkg.mSigningKeys;
- }
+ return keys;
}
/**
* Collect certificates from all the APKs described in the given package,
- * populating {@link Package#mSignatures}. Also asserts that all APK
+ * populating {@link Package#mSigningDetails}. Also asserts that all APK
* contents are signed correctly and consistently.
*/
public static void collectCertificates(Package pkg, @ParseFlags int parseFlags)
@@ -1528,17 +1502,13 @@
final int childCount = (pkg.childPackages != null) ? pkg.childPackages.size() : 0;
for (int i = 0; i < childCount; i++) {
Package childPkg = pkg.childPackages.get(i);
- childPkg.mCertificates = pkg.mCertificates;
- childPkg.mSignatures = pkg.mSignatures;
- childPkg.mSigningKeys = pkg.mSigningKeys;
+ childPkg.mSigningDetails = pkg.mSigningDetails;
}
}
private static void collectCertificatesInternal(Package pkg, @ParseFlags int parseFlags)
throws PackageParserException {
- pkg.mCertificates = null;
- pkg.mSignatures = null;
- pkg.mSigningKeys = null;
+ pkg.mSigningDetails = SigningDetails.UNKNOWN;
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "collectCertificates");
try {
@@ -1558,12 +1528,12 @@
throws PackageParserException {
final String apkPath = apkFile.getAbsolutePath();
- int minSignatureScheme = ApkSignatureVerifier.VERSION_JAR_SIGNATURE_SCHEME;
+ int minSignatureScheme = SigningDetails.SignatureSchemeVersion.JAR;
if (pkg.applicationInfo.isStaticSharedLibrary()) {
// must use v2 signing scheme
- minSignatureScheme = ApkSignatureVerifier.VERSION_APK_SIGNATURE_SCHEME_V2;
+ minSignatureScheme = SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2;
}
- ApkSignatureVerifier.Result verified;
+ SigningDetails verified;
if ((parseFlags & PARSE_IS_SYSTEM_DIR) != 0) {
// systemDir APKs are already trusted, save time by not verifying
verified = ApkSignatureVerifier.plsCertsNoVerifyOnlyCerts(
@@ -1571,29 +1541,14 @@
} else {
verified = ApkSignatureVerifier.verify(apkPath, minSignatureScheme);
}
- if (verified.signatureSchemeVersion
- < ApkSignatureVerifier.VERSION_APK_SIGNATURE_SCHEME_V2) {
- // TODO (b/68860689): move this logic to packagemanagerserivce
- if ((parseFlags & PARSE_IS_EPHEMERAL) != 0) {
- throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
- "No APK Signature Scheme v2 signature in ephemeral package " + apkPath);
- }
- }
// Verify that entries are signed consistently with the first pkg
// we encountered. Note that for splits, certificates may have
// already been populated during an earlier parse of a base APK.
- if (pkg.mCertificates == null) {
- pkg.mCertificates = verified.certs;
- pkg.mSignatures = verified.sigs;
- pkg.mSigningKeys = new ArraySet<>(verified.certs.length);
- for (int i = 0; i < verified.certs.length; i++) {
- Certificate[] signerCerts = verified.certs[i];
- Certificate signerCert = signerCerts[0];
- pkg.mSigningKeys.add(signerCert.getPublicKey());
- }
+ if (pkg.mSigningDetails == SigningDetails.UNKNOWN) {
+ pkg.mSigningDetails = verified;
} else {
- if (!Signature.areExactMatch(pkg.mSignatures, verified.sigs)) {
+ if (!Signature.areExactMatch(pkg.mSigningDetails.signatures, verified.signatures)) {
throw new PackageParserException(
INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
apkPath + " has mismatched certificates");
@@ -1655,8 +1610,7 @@
parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);
- final Signature[] signatures;
- final Certificate[][] certificates;
+ final SigningDetails signingDetails;
if ((flags & PARSE_COLLECT_CERTIFICATES) != 0) {
// TODO: factor signature related items out of Package object
final Package tempPkg = new Package((String) null);
@@ -1666,15 +1620,13 @@
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
- signatures = tempPkg.mSignatures;
- certificates = tempPkg.mCertificates;
+ signingDetails = tempPkg.mSigningDetails;
} else {
- signatures = null;
- certificates = null;
+ signingDetails = SigningDetails.UNKNOWN;
}
final AttributeSet attrs = parser;
- return parseApkLite(apkPath, parser, attrs, signatures, certificates);
+ return parseApkLite(apkPath, parser, attrs, signingDetails);
} catch (XmlPullParserException | IOException | RuntimeException e) {
Slog.w(TAG, "Failed to parse " + apkPath, e);
@@ -1761,7 +1713,7 @@
}
private static ApkLite parseApkLite(String codePath, XmlPullParser parser, AttributeSet attrs,
- Signature[] signatures, Certificate[][] certificates)
+ SigningDetails signingDetails)
throws IOException, XmlPullParserException, PackageParserException {
final Pair<String, String> packageSplit = parsePackageSplitNames(parser, attrs);
@@ -1854,7 +1806,7 @@
return new ApkLite(codePath, packageSplit.first, packageSplit.second, isFeatureSplit,
configForSplit, usesSplitName, versionCode, versionCodeMajor, revisionCode,
- installLocation, verifiers, signatures, certificates, coreApp, debuggable,
+ installLocation, verifiers, signingDetails, coreApp, debuggable,
multiArch, use32bitAbi, extractNativeLibs, isolatedSplits);
}
@@ -2039,11 +1991,6 @@
String str = sa.getNonConfigurationString(
com.android.internal.R.styleable.AndroidManifest_sharedUserId, 0);
if (str != null && str.length() > 0) {
- if ((flags & PARSE_IS_EPHEMERAL) != 0) {
- outError[0] = "sharedUserId not allowed in ephemeral application";
- mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID;
- return null;
- }
String nameError = validateName(str, true, false);
if (nameError != null && !"android".equals(pkg.packageName)) {
outError[0] = "<manifest> specifies bad sharedUserId name \""
@@ -2130,7 +2077,7 @@
pkg.mOverlayPriority = sa.getInt(
com.android.internal.R.styleable.AndroidManifestResourceOverlay_priority,
0);
- pkg.mIsStaticOverlay = sa.getBoolean(
+ pkg.mOverlayIsStatic = sa.getBoolean(
com.android.internal.R.styleable.AndroidManifestResourceOverlay_isStatic,
false);
final String propName = sa.getString(
@@ -2304,8 +2251,9 @@
return null;
}
+ boolean defaultToCurrentDevBranch = (flags & PARSE_FORCE_SDK) != 0;
final int targetSdkVersion = PackageParser.computeTargetSdkVersion(targetVers,
- targetCode, SDK_VERSION, SDK_CODENAMES, outError);
+ targetCode, SDK_CODENAMES, outError, defaultToCurrentDevBranch);
if (targetSdkVersion < 0) {
mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK;
return null;
@@ -2621,19 +2569,19 @@
* application manifest, or 0 otherwise
* @param targetCode targetSdkVersion code, if specified in the application
* manifest, or {@code null} otherwise
- * @param platformSdkVersion platform SDK version number, typically
- * Build.VERSION.SDK_INT
* @param platformSdkCodenames array of allowed pre-release SDK codenames
* for this platform
* @param outError output array to populate with error, if applicable
+ * @param forceCurrentDev if development target code is not available, use the current
+ * development version by default.
* @return the targetSdkVersion to use at runtime, or -1 if the package is
* not compatible with this platform
* @hide Exposed for unit testing only.
*/
@TestApi
public static int computeTargetSdkVersion(@IntRange(from = 0) int targetVers,
- @Nullable String targetCode, @IntRange(from = 1) int platformSdkVersion,
- @NonNull String[] platformSdkCodenames, @NonNull String[] outError) {
+ @Nullable String targetCode, @NonNull String[] platformSdkCodenames,
+ @NonNull String[] outError, boolean forceCurrentDev) {
// If it's a release SDK, return the version number unmodified.
if (targetCode == null) {
return targetVers;
@@ -2641,7 +2589,7 @@
// If it's a pre-release SDK and the codename matches this platform, it
// definitely targets this SDK.
- if (ArrayUtils.contains(platformSdkCodenames, targetCode)) {
+ if (ArrayUtils.contains(platformSdkCodenames, targetCode) || forceCurrentDev) {
return Build.VERSION_CODES.CUR_DEVELOPMENT;
}
@@ -5734,6 +5682,261 @@
return true;
}
+ /** A container for signing-related data of an application package. */
+ public static final class SigningDetails implements Parcelable {
+
+ @IntDef({SigningDetails.SignatureSchemeVersion.UNKNOWN,
+ SigningDetails.SignatureSchemeVersion.JAR,
+ SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2,
+ SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V3})
+ public @interface SignatureSchemeVersion {
+ int UNKNOWN = 0;
+ int JAR = 1;
+ int SIGNING_BLOCK_V2 = 2;
+ int SIGNING_BLOCK_V3 = 3;
+ }
+
+ @Nullable
+ public final Signature[] signatures;
+ @SignatureSchemeVersion
+ public final int signatureSchemeVersion;
+ @Nullable
+ public final ArraySet<PublicKey> publicKeys;
+
+ /**
+ * Collection of {@code Signature} objects, each of which is formed from a former signing
+ * certificate of this APK before it was changed by signing certificate rotation.
+ */
+ @Nullable
+ public final Signature[] pastSigningCertificates;
+
+ /**
+ * Flags for the {@code pastSigningCertificates} collection, which indicate the capabilities
+ * the including APK wishes to grant to its past signing certificates.
+ */
+ @Nullable
+ public final int[] pastSigningCertificatesFlags;
+
+ /** A representation of unknown signing details. Use instead of null. */
+ public static final SigningDetails UNKNOWN =
+ new SigningDetails(null, SignatureSchemeVersion.UNKNOWN, null, null, null);
+
+ @VisibleForTesting
+ public SigningDetails(Signature[] signatures,
+ @SignatureSchemeVersion int signatureSchemeVersion,
+ ArraySet<PublicKey> keys, Signature[] pastSigningCertificates,
+ int[] pastSigningCertificatesFlags) {
+ this.signatures = signatures;
+ this.signatureSchemeVersion = signatureSchemeVersion;
+ this.publicKeys = keys;
+ this.pastSigningCertificates = pastSigningCertificates;
+ this.pastSigningCertificatesFlags = pastSigningCertificatesFlags;
+ }
+
+ public SigningDetails(Signature[] signatures,
+ @SignatureSchemeVersion int signatureSchemeVersion,
+ Signature[] pastSigningCertificates, int[] pastSigningCertificatesFlags)
+ throws CertificateException {
+ this(signatures, signatureSchemeVersion, toSigningKeys(signatures),
+ pastSigningCertificates, pastSigningCertificatesFlags);
+ }
+
+ public SigningDetails(Signature[] signatures,
+ @SignatureSchemeVersion int signatureSchemeVersion)
+ throws CertificateException {
+ this(signatures, signatureSchemeVersion,
+ null, null);
+ }
+
+ public SigningDetails(SigningDetails orig) {
+ if (orig != null) {
+ if (orig.signatures != null) {
+ this.signatures = orig.signatures.clone();
+ } else {
+ this.signatures = null;
+ }
+ this.signatureSchemeVersion = orig.signatureSchemeVersion;
+ this.publicKeys = new ArraySet<>(orig.publicKeys);
+ if (orig.pastSigningCertificates != null) {
+ this.pastSigningCertificates = orig.pastSigningCertificates.clone();
+ this.pastSigningCertificatesFlags = orig.pastSigningCertificatesFlags.clone();
+ } else {
+ this.pastSigningCertificates = null;
+ this.pastSigningCertificatesFlags = null;
+ }
+ } else {
+ this.signatures = null;
+ this.signatureSchemeVersion = SignatureSchemeVersion.UNKNOWN;
+ this.publicKeys = null;
+ this.pastSigningCertificates = null;
+ this.pastSigningCertificatesFlags = null;
+ }
+ }
+
+ /** Returns true if the signing details have one or more signatures. */
+ public boolean hasSignatures() {
+ return signatures != null && signatures.length > 0;
+ }
+
+ /** Returns true if the signing details have past signing certificates. */
+ public boolean hasPastSigningCertificates() {
+ return pastSigningCertificates != null && pastSigningCertificates.length > 0;
+ }
+
+ /** Returns true if the signatures in this and other match exactly. */
+ public boolean signaturesMatchExactly(SigningDetails other) {
+ return Signature.areExactMatch(this.signatures, other.signatures);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ boolean isUnknown = UNKNOWN == this;
+ dest.writeBoolean(isUnknown);
+ if (isUnknown) {
+ return;
+ }
+ dest.writeTypedArray(this.signatures, flags);
+ dest.writeInt(this.signatureSchemeVersion);
+ dest.writeArraySet(this.publicKeys);
+ dest.writeTypedArray(this.pastSigningCertificates, flags);
+ dest.writeIntArray(this.pastSigningCertificatesFlags);
+ }
+
+ protected SigningDetails(Parcel in) {
+ final ClassLoader boot = Object.class.getClassLoader();
+ this.signatures = in.createTypedArray(Signature.CREATOR);
+ this.signatureSchemeVersion = in.readInt();
+ this.publicKeys = (ArraySet<PublicKey>) in.readArraySet(boot);
+ this.pastSigningCertificates = in.createTypedArray(Signature.CREATOR);
+ this.pastSigningCertificatesFlags = in.createIntArray();
+ }
+
+ public static final Creator<SigningDetails> CREATOR = new Creator<SigningDetails>() {
+ @Override
+ public SigningDetails createFromParcel(Parcel source) {
+ if (source.readBoolean()) {
+ return UNKNOWN;
+ }
+ return new SigningDetails(source);
+ }
+
+ @Override
+ public SigningDetails[] newArray(int size) {
+ return new SigningDetails[size];
+ }
+ };
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SigningDetails)) return false;
+
+ SigningDetails that = (SigningDetails) o;
+
+ if (signatureSchemeVersion != that.signatureSchemeVersion) return false;
+ if (!Signature.areExactMatch(signatures, that.signatures)) return false;
+ if (publicKeys != null) {
+ if (!publicKeys.equals((that.publicKeys))) {
+ return false;
+ }
+ } else if (that.publicKeys != null) {
+ return false;
+ }
+
+ // can't use Signature.areExactMatch() because order matters with the past signing certs
+ if (!Arrays.equals(pastSigningCertificates, that.pastSigningCertificates)) {
+ return false;
+ }
+ if (!Arrays.equals(pastSigningCertificatesFlags, that.pastSigningCertificatesFlags)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = +Arrays.hashCode(signatures);
+ result = 31 * result + signatureSchemeVersion;
+ result = 31 * result + (publicKeys != null ? publicKeys.hashCode() : 0);
+ result = 31 * result + Arrays.hashCode(pastSigningCertificates);
+ result = 31 * result + Arrays.hashCode(pastSigningCertificatesFlags);
+ return result;
+ }
+
+ /**
+ * Builder of {@code SigningDetails} instances.
+ */
+ public static class Builder {
+ private Signature[] mSignatures;
+ private int mSignatureSchemeVersion = SignatureSchemeVersion.UNKNOWN;
+ private Signature[] mPastSigningCertificates;
+ private int[] mPastSigningCertificatesFlags;
+
+ public Builder() {
+ }
+
+ /** get signing certificates used to sign the current APK */
+ public Builder setSignatures(Signature[] signatures) {
+ mSignatures = signatures;
+ return this;
+ }
+
+ /** set the signature scheme version used to sign the APK */
+ public Builder setSignatureSchemeVersion(int signatureSchemeVersion) {
+ mSignatureSchemeVersion = signatureSchemeVersion;
+ return this;
+ }
+
+ /** set the signing certificates by which the APK proved it can be authenticated */
+ public Builder setPastSigningCertificates(Signature[] pastSigningCertificates) {
+ mPastSigningCertificates = pastSigningCertificates;
+ return this;
+ }
+
+ /** set the flags for the {@code pastSigningCertificates} */
+ public Builder setPastSigningCertificatesFlags(int[] pastSigningCertificatesFlags) {
+ mPastSigningCertificatesFlags = pastSigningCertificatesFlags;
+ return this;
+ }
+
+ private void checkInvariants() {
+ // must have signatures and scheme version set
+ if (mSignatures == null) {
+ throw new IllegalStateException("SigningDetails requires the current signing"
+ + " certificates.");
+ }
+
+ // pastSigningCerts and flags must match up
+ boolean pastMismatch = false;
+ if (mPastSigningCertificates != null && mPastSigningCertificatesFlags != null) {
+ if (mPastSigningCertificates.length != mPastSigningCertificatesFlags.length) {
+ pastMismatch = true;
+ }
+ } else if (!(mPastSigningCertificates == null
+ && mPastSigningCertificatesFlags == null)) {
+ pastMismatch = true;
+ }
+ if (pastMismatch) {
+ throw new IllegalStateException("SigningDetails must have a one to one mapping "
+ + "between pastSigningCertificates and pastSigningCertificatesFlags");
+ }
+ }
+ /** build a {@code SigningDetails} object */
+ public SigningDetails build()
+ throws CertificateException {
+ checkInvariants();
+ return new SigningDetails(mSignatures, mSignatureSchemeVersion,
+ mPastSigningCertificates, mPastSigningCertificatesFlags);
+ }
+ }
+ }
+
/**
* Representation of a full package parsed from APK files on disk. A package
* consists of a single base APK, and zero or more split APKs.
@@ -5840,8 +6043,7 @@
public int mSharedUserLabel;
// Signatures that were read from the package.
- public Signature[] mSignatures;
- public Certificate[][] mCertificates;
+ @NonNull public SigningDetails mSigningDetails = SigningDetails.UNKNOWN;
// For use by package manager service for quick lookup of
// preferred up order.
@@ -5884,8 +6086,7 @@
public String mOverlayTarget;
public int mOverlayPriority;
- public boolean mIsStaticOverlay;
- public boolean mTrustedOverlay;
+ public boolean mOverlayIsStatic;
public int mCompileSdkVersion;
public String mCompileSdkVersionCodename;
@@ -5893,7 +6094,6 @@
/**
* Data used to feed the KeySetManagerService
*/
- public ArraySet<PublicKey> mSigningKeys;
public ArraySet<String> mUpgradeKeySets;
public ArrayMap<String, ArraySet<PublicKey>> mKeySetMapping;
@@ -5950,6 +6150,8 @@
}
}
+ /** @deprecated Forward locked apps no longer supported. Resource path not needed. */
+ @Deprecated
public void setApplicationInfoResourcePath(String resourcePath) {
this.applicationInfo.setResourcePath(resourcePath);
if (childPackages != null) {
@@ -5960,6 +6162,8 @@
}
}
+ /** @deprecated Forward locked apps no longer supported. Resource path not needed. */
+ @Deprecated
public void setApplicationInfoBaseResourcePath(String resourcePath) {
this.applicationInfo.setBaseResourcePath(resourcePath);
if (childPackages != null) {
@@ -6008,6 +6212,8 @@
// Children have no splits
}
+ /** @deprecated Forward locked apps no longer supported. Resource path not needed. */
+ @Deprecated
public void setApplicationInfoSplitResourcePaths(String[] resroucePaths) {
this.applicationInfo.setSplitResourcePaths(resroucePaths);
// Children have no splits
@@ -6037,12 +6243,13 @@
}
}
- public void setSignatures(Signature[] signatures) {
- this.mSignatures = signatures;
+ /** Sets signing details on the package and any of its children. */
+ public void setSigningDetails(@NonNull SigningDetails signingDetails) {
+ mSigningDetails = signingDetails;
if (childPackages != null) {
final int packageCount = childPackages.size();
for (int i = 0; i < packageCount; i++) {
- childPackages.get(i).mSignatures = signatures;
+ childPackages.get(i).mSigningDetails = signingDetails;
}
}
}
@@ -6243,6 +6450,31 @@
+ " " + packageName + "}";
}
+ public String dumpState_temp() {
+ String flags = "";
+ flags += ((applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 ? "U" : "");
+ flags += ((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 ? "S" : "");
+ if ("".equals(flags)) {
+ flags = "-";
+ }
+ String privFlags = "";
+ privFlags += ((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0 ? "P" : "");
+ privFlags += ((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_OEM) != 0 ? "O" : "");
+ privFlags += ((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0 ? "V" : "");
+ if ("".equals(privFlags)) {
+ privFlags = "-";
+ }
+ return "Package{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + packageName
+ + ", ver:" + getLongVersionCode()
+ + ", path: " + codePath
+ + ", flags: " + flags
+ + ", privFlags: " + privFlags
+ + ", extra: " + (mExtras == null ? "<<NULL>>" : Integer.toHexString(System.identityHashCode(mExtras)) + "}")
+ + "}";
+ }
+
@Override
public int describeContents() {
return 0;
@@ -6348,8 +6580,7 @@
}
mSharedUserLabel = dest.readInt();
- mSignatures = (Signature[]) dest.readParcelableArray(boot, Signature.class);
- mCertificates = (Certificate[][]) dest.readSerializable();
+ mSigningDetails = dest.readParcelable(boot);
mPreferredOrder = dest.readInt();
@@ -6385,11 +6616,9 @@
mRequiredAccountType = dest.readString();
mOverlayTarget = dest.readString();
mOverlayPriority = dest.readInt();
- mIsStaticOverlay = (dest.readInt() == 1);
- mTrustedOverlay = (dest.readInt() == 1);
+ mOverlayIsStatic = (dest.readInt() == 1);
mCompileSdkVersion = dest.readInt();
mCompileSdkVersionCodename = dest.readString();
- mSigningKeys = (ArraySet<PublicKey>) dest.readArraySet(boot);
mUpgradeKeySets = (ArraySet<String>) dest.readArraySet(boot);
mKeySetMapping = readKeySetMapping(dest);
@@ -6489,8 +6718,7 @@
dest.writeString(mSharedUserId);
dest.writeInt(mSharedUserLabel);
- dest.writeParcelableArray(mSignatures, flags);
- dest.writeSerializable(mCertificates);
+ dest.writeParcelable(mSigningDetails, flags);
dest.writeInt(mPreferredOrder);
@@ -6511,11 +6739,9 @@
dest.writeString(mRequiredAccountType);
dest.writeString(mOverlayTarget);
dest.writeInt(mOverlayPriority);
- dest.writeInt(mIsStaticOverlay ? 1 : 0);
- dest.writeInt(mTrustedOverlay ? 1 : 0);
+ dest.writeInt(mOverlayIsStatic ? 1 : 0);
dest.writeInt(mCompileSdkVersion);
dest.writeString(mCompileSdkVersionCodename);
- dest.writeArraySet(mSigningKeys);
dest.writeArraySet(mUpgradeKeySets);
writeKeySetMapping(dest, mKeySetMapping);
dest.writeString(cpuAbiOverride);
diff --git a/android/content/pm/PackageSharedLibraryUpdater.java b/android/content/pm/PackageSharedLibraryUpdater.java
new file mode 100644
index 0000000..49d884c
--- /dev/null
+++ b/android/content/pm/PackageSharedLibraryUpdater.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 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.content.pm;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.ArrayList;
+
+/**
+ * Base for classes that update a {@link PackageParser.Package}'s shared libraries.
+ *
+ * @hide
+ */
+@VisibleForTesting
+public abstract class PackageSharedLibraryUpdater {
+
+ /**
+ * Update the package's shared libraries.
+ *
+ * @param pkg the package to update.
+ */
+ public abstract void updatePackage(PackageParser.Package pkg);
+
+ static @NonNull
+ <T> ArrayList<T> prefix(@Nullable ArrayList<T> cur, T val) {
+ if (cur == null) {
+ cur = new ArrayList<>();
+ }
+ cur.add(0, val);
+ return cur;
+ }
+
+ static boolean isLibraryPresent(ArrayList<String> usesLibraries,
+ ArrayList<String> usesOptionalLibraries, String apacheHttpLegacy) {
+ return ArrayUtils.contains(usesLibraries, apacheHttpLegacy)
+ || ArrayUtils.contains(usesOptionalLibraries, apacheHttpLegacy);
+ }
+}
diff --git a/android/content/pm/PackageUserState.java b/android/content/pm/PackageUserState.java
index 069b2d4..293beb2 100644
--- a/android/content/pm/PackageUserState.java
+++ b/android/content/pm/PackageUserState.java
@@ -52,6 +52,7 @@
public int appLinkGeneration;
public int categoryHint = ApplicationInfo.CATEGORY_UNDEFINED;
public int installReason;
+ public String harmfulAppWarning;
public ArraySet<String> disabledComponents;
public ArraySet<String> enabledComponents;
@@ -87,6 +88,7 @@
enabledComponents = ArrayUtils.cloneOrNull(o.enabledComponents);
overlayPaths =
o.overlayPaths == null ? null : Arrays.copyOf(o.overlayPaths, o.overlayPaths.length);
+ harmfulAppWarning = o.harmfulAppWarning;
}
/**
@@ -247,6 +249,11 @@
}
}
}
+ if (harmfulAppWarning == null && oldState.harmfulAppWarning != null
+ || (harmfulAppWarning != null
+ && !harmfulAppWarning.equals(oldState.harmfulAppWarning))) {
+ return false;
+ }
return true;
}
}
diff --git a/android/content/pm/ShortcutInfo.java b/android/content/pm/ShortcutInfo.java
index 8839cf9..ea476b0 100644
--- a/android/content/pm/ShortcutInfo.java
+++ b/android/content/pm/ShortcutInfo.java
@@ -181,6 +181,11 @@
public static final int DISABLED_REASON_APP_CHANGED = 2;
/**
+ * Shortcut is disabled for an unknown reason.
+ */
+ public static final int DISABLED_REASON_UNKNOWN = 3;
+
+ /**
* A disabled reason that's equal to or bigger than this is due to backup and restore issue.
* A shortcut with such a reason wil be visible to the launcher, but not to the publisher.
* ({@link #isVisibleToPublisher()} will be false.)
@@ -214,6 +219,7 @@
DISABLED_REASON_NOT_DISABLED,
DISABLED_REASON_BY_APP,
DISABLED_REASON_APP_CHANGED,
+ DISABLED_REASON_UNKNOWN,
DISABLED_REASON_VERSION_LOWER,
DISABLED_REASON_BACKUP_NOT_SUPPORTED,
DISABLED_REASON_SIGNATURE_MISMATCH,
@@ -272,6 +278,9 @@
case DISABLED_REASON_OTHER_RESTORE_ISSUE:
return res.getString(
com.android.internal.R.string.shortcut_restore_unknown_issue);
+ case DISABLED_REASON_UNKNOWN:
+ return res.getString(
+ com.android.internal.R.string.shortcut_disabled_reason_unknown);
}
return null;
}
diff --git a/android/content/pm/dex/ArtManager.java b/android/content/pm/dex/ArtManager.java
index 201cd8d..aa9c46e 100644
--- a/android/content/pm/dex/ArtManager.java
+++ b/android/content/pm/dex/ArtManager.java
@@ -153,4 +153,14 @@
return true;
}
}
+
+ /**
+ * Return the profile name for the given split. If {@code splitName} is null the
+ * method returns the profile name for the base apk.
+ *
+ * @hide
+ */
+ public static String getProfileName(String splitName) {
+ return splitName == null ? "primary.prof" : splitName + ".split.prof";
+ }
}
diff --git a/android/content/pm/dex/DexMetadataHelper.java b/android/content/pm/dex/DexMetadataHelper.java
new file mode 100644
index 0000000..5d10b88
--- /dev/null
+++ b/android/content/pm/dex/DexMetadataHelper.java
@@ -0,0 +1,230 @@
+/**
+ * Copyright 2018 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.content.pm.dex;
+
+import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_DEX_METADATA;
+import static android.content.pm.PackageParser.APK_FILE_EXTENSION;
+
+import android.content.pm.PackageParser;
+import android.content.pm.PackageParser.PackageLite;
+import android.content.pm.PackageParser.PackageParserException;
+import android.util.ArrayMap;
+import android.util.jar.StrictJarFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Helper class used to compute and validate the location of dex metadata files.
+ *
+ * @hide
+ */
+public class DexMetadataHelper {
+ private static final String DEX_METADATA_FILE_EXTENSION = ".dm";
+
+ private DexMetadataHelper() {}
+
+ /** Return true if the given file is a dex metadata file. */
+ public static boolean isDexMetadataFile(File file) {
+ return isDexMetadataPath(file.getName());
+ }
+
+ /** Return true if the given path is a dex metadata path. */
+ private static boolean isDexMetadataPath(String path) {
+ return path.endsWith(DEX_METADATA_FILE_EXTENSION);
+ }
+
+ /**
+ * Return the size (in bytes) of all dex metadata files associated with the given package.
+ */
+ public static long getPackageDexMetadataSize(PackageLite pkg) {
+ long sizeBytes = 0;
+ Collection<String> dexMetadataList = DexMetadataHelper.getPackageDexMetadata(pkg).values();
+ for (String dexMetadata : dexMetadataList) {
+ sizeBytes += new File(dexMetadata).length();
+ }
+ return sizeBytes;
+ }
+
+ /**
+ * Search for the dex metadata file associated with the given target file.
+ * If it exists, the method returns the dex metadata file; otherwise it returns null.
+ *
+ * Note that this performs a loose matching suitable to be used in the InstallerSession logic.
+ * i.e. the method will attempt to match the {@code dmFile} regardless of {@code targetFile}
+ * extension (e.g. 'foo.dm' will match 'foo' or 'foo.apk').
+ */
+ public static File findDexMetadataForFile(File targetFile) {
+ String dexMetadataPath = buildDexMetadataPathForFile(targetFile);
+ File dexMetadataFile = new File(dexMetadataPath);
+ return dexMetadataFile.exists() ? dexMetadataFile : null;
+ }
+
+ /**
+ * Return the dex metadata files for the given package as a map
+ * [code path -> dex metadata path].
+ *
+ * NOTE: involves I/O checks.
+ */
+ public static Map<String, String> getPackageDexMetadata(PackageParser.Package pkg) {
+ return buildPackageApkToDexMetadataMap(pkg.getAllCodePaths());
+ }
+
+ /**
+ * Return the dex metadata files for the given package as a map
+ * [code path -> dex metadata path].
+ *
+ * NOTE: involves I/O checks.
+ */
+ private static Map<String, String> getPackageDexMetadata(PackageLite pkg) {
+ return buildPackageApkToDexMetadataMap(pkg.getAllCodePaths());
+ }
+
+ /**
+ * Look up the dex metadata files for the given code paths building the map
+ * [code path -> dex metadata].
+ *
+ * For each code path (.apk) the method checks if a matching dex metadata file (.dm) exists.
+ * If it does it adds the pair to the returned map.
+ *
+ * Note that this method will do a loose
+ * matching based on the extension ('foo.dm' will match 'foo.apk' or 'foo').
+ *
+ * This should only be used for code paths extracted from a package structure after the naming
+ * was enforced in the installer.
+ */
+ private static Map<String, String> buildPackageApkToDexMetadataMap(
+ List<String> codePaths) {
+ ArrayMap<String, String> result = new ArrayMap<>();
+ for (int i = codePaths.size() - 1; i >= 0; i--) {
+ String codePath = codePaths.get(i);
+ String dexMetadataPath = buildDexMetadataPathForFile(new File(codePath));
+
+ if (Files.exists(Paths.get(dexMetadataPath))) {
+ result.put(codePath, dexMetadataPath);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Return the dex metadata path associated with the given code path.
+ * (replaces '.apk' extension with '.dm')
+ *
+ * @throws IllegalArgumentException if the code path is not an .apk.
+ */
+ public static String buildDexMetadataPathForApk(String codePath) {
+ if (!PackageParser.isApkPath(codePath)) {
+ throw new IllegalStateException(
+ "Corrupted package. Code path is not an apk " + codePath);
+ }
+ return codePath.substring(0, codePath.length() - APK_FILE_EXTENSION.length())
+ + DEX_METADATA_FILE_EXTENSION;
+ }
+
+ /**
+ * Return the dex metadata path corresponding to the given {@code targetFile} using a loose
+ * matching.
+ * i.e. the method will attempt to match the {@code dmFile} regardless of {@code targetFile}
+ * extension (e.g. 'foo.dm' will match 'foo' or 'foo.apk').
+ */
+ private static String buildDexMetadataPathForFile(File targetFile) {
+ return PackageParser.isApkFile(targetFile)
+ ? buildDexMetadataPathForApk(targetFile.getPath())
+ : targetFile.getPath() + DEX_METADATA_FILE_EXTENSION;
+ }
+
+ /**
+ * Validate the dex metadata files installed for the given package.
+ *
+ * @throws PackageParserException in case of errors.
+ */
+ public static void validatePackageDexMetadata(PackageParser.Package pkg)
+ throws PackageParserException {
+ Collection<String> apkToDexMetadataList = getPackageDexMetadata(pkg).values();
+ for (String dexMetadata : apkToDexMetadataList) {
+ validateDexMetadataFile(dexMetadata);
+ }
+ }
+
+ /**
+ * Validate that the given file is a dex metadata archive.
+ * This is just a sanity validation that the file is a zip archive.
+ *
+ * @throws PackageParserException if the file is not a .dm file.
+ */
+ private static void validateDexMetadataFile(String dmaPath) throws PackageParserException {
+ StrictJarFile jarFile = null;
+ try {
+ jarFile = new StrictJarFile(dmaPath, false, false);
+ } catch (IOException e) {
+ throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA,
+ "Error opening " + dmaPath, e);
+ } finally {
+ if (jarFile != null) {
+ try {
+ jarFile.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Validates that all dex metadata paths in the given list have a matching apk.
+ * (for any foo.dm there should be either a 'foo' of a 'foo.apk' file).
+ * If that's not the case it throws {@code IllegalStateException}.
+ *
+ * This is used to perform a basic sanity check during adb install commands.
+ * (The installer does not support stand alone .dm files)
+ */
+ public static void validateDexPaths(String[] paths) {
+ ArrayList<String> apks = new ArrayList<>();
+ for (int i = 0; i < paths.length; i++) {
+ if (PackageParser.isApkPath(paths[i])) {
+ apks.add(paths[i]);
+ }
+ }
+ ArrayList<String> unmatchedDmFiles = new ArrayList<>();
+ for (int i = 0; i < paths.length; i++) {
+ String dmPath = paths[i];
+ if (isDexMetadataPath(dmPath)) {
+ boolean valid = false;
+ for (int j = apks.size() - 1; j >= 0; j--) {
+ if (dmPath.equals(buildDexMetadataPathForFile(new File(apks.get(j))))) {
+ valid = true;
+ break;
+ }
+ }
+ if (!valid) {
+ unmatchedDmFiles.add(dmPath);
+ }
+ }
+ }
+ if (!unmatchedDmFiles.isEmpty()) {
+ throw new IllegalStateException("Unmatched .dm files: " + unmatchedDmFiles);
+ }
+ }
+
+}
diff --git a/android/content/res/BridgeAssetManager.java b/android/content/res/BridgeAssetManager.java
index 2691e56..a1a4a19 100644
--- a/android/content/res/BridgeAssetManager.java
+++ b/android/content/res/BridgeAssetManager.java
@@ -36,7 +36,6 @@
// Note that AssetManager() creates a system AssetManager and we override it
// with our BridgeAssetManager.
AssetManager.sSystem = new BridgeAssetManager();
- AssetManager.sSystem.makeStringBlocks(null);
}
return AssetManager.sSystem;
}
diff --git a/android/content/res/BridgeTypedArray.java b/android/content/res/BridgeTypedArray.java
index 5536c4f..9505993 100644
--- a/android/content/res/BridgeTypedArray.java
+++ b/android/content/res/BridgeTypedArray.java
@@ -29,7 +29,6 @@
import com.android.resources.ResourceType;
import android.annotation.Nullable;
-import android.content.res.Resources.NotFoundException;
import android.content.res.Resources.Theme;
import android.graphics.Typeface;
import android.graphics.Typeface_Accessor;
@@ -676,6 +675,13 @@
return idValue;
}
+ if ("text".equals(mNames[index])) {
+ // In a TextView, if the text is set from the attribute android:text, the correct
+ // behaviour is not to find a resourceId for the text, and to return the default value.
+ // So in this case, do not log a warning.
+ return defValue;
+ }
+
Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_RESOLVE,
String.format(
"Unable to resolve id \"%1$s\" for attribute \"%2$s\"", value, mNames[index]),
diff --git a/android/content/res/ResourcesImpl.java b/android/content/res/ResourcesImpl.java
index 3239212..97cb78b 100644
--- a/android/content/res/ResourcesImpl.java
+++ b/android/content/res/ResourcesImpl.java
@@ -815,7 +815,7 @@
} finally {
stack.pop();
}
- } catch (Exception e) {
+ } catch (Exception | StackOverflowError e) {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
final NotFoundException rnf = new NotFoundException(
"File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
diff --git a/android/content/res/Resources_Delegate.java b/android/content/res/Resources_Delegate.java
index a32d528..77ae90f 100644
--- a/android/content/res/Resources_Delegate.java
+++ b/android/content/res/Resources_Delegate.java
@@ -886,6 +886,12 @@
}
@LayoutlibDelegate
+ static void getValueForDensity(Resources resources, int id, int density, TypedValue outValue,
+ boolean resolveRefs) throws NotFoundException {
+ getValue(resources, id, outValue, resolveRefs);
+ }
+
+ @LayoutlibDelegate
static XmlResourceParser getXml(Resources resources, int id) throws NotFoundException {
Pair<String, ResourceValue> v = getResourceValue(resources, id, mPlatformResourceFlag);
diff --git a/android/database/sqlite/SQLiteOpenHelper.java b/android/database/sqlite/SQLiteOpenHelper.java
index 49f357e..a2991e6 100644
--- a/android/database/sqlite/SQLiteOpenHelper.java
+++ b/android/database/sqlite/SQLiteOpenHelper.java
@@ -66,7 +66,7 @@
* created or opened until one of {@link #getWritableDatabase} or
* {@link #getReadableDatabase} is called.
*
- * @param context to use to open or create the database
+ * @param context to use for locating paths to the the database
* @param name of the database file, or null for an in-memory database
* @param factory to use for creating cursor objects, or null for the default
* @param version number of the database (starting at 1); if the database is older,
@@ -86,7 +86,7 @@
* <p>Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be
* used to handle corruption when sqlite reports database corruption.</p>
*
- * @param context to use to open or create the database
+ * @param context to use for locating paths to the the database
* @param name of the database file, or null for an in-memory database
* @param factory to use for creating cursor objects, or null for the default
* @param version number of the database (starting at 1); if the database is older,
@@ -107,7 +107,7 @@
* created or opened until one of {@link #getWritableDatabase} or
* {@link #getReadableDatabase} is called.
*
- * @param context to use to open or create the database
+ * @param context to use for locating paths to the the database
* @param name of the database file, or null for an in-memory database
* @param version number of the database (starting at 1); if the database is older,
* {@link #onUpgrade} will be used to upgrade the database; if the database is
@@ -128,7 +128,7 @@
* minimumSupportedVersion is found, it is simply deleted and a new database is created with the
* given name and version
*
- * @param context to use to open or create the database
+ * @param context to use for locating paths to the the database
* @param name the name of the database file, null for a temporary in-memory database
* @param factory to use for creating cursor objects, null for default
* @param version the required version of the database
diff --git a/android/ext/services/autofill/AutofillFieldClassificationServiceImpl.java b/android/ext/services/autofill/AutofillFieldClassificationServiceImpl.java
new file mode 100644
index 0000000..4709d35
--- /dev/null
+++ b/android/ext/services/autofill/AutofillFieldClassificationServiceImpl.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2018 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.ext.services.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.service.autofill.AutofillFieldClassificationService;
+import android.util.Log;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.util.List;
+
+public class AutofillFieldClassificationServiceImpl extends AutofillFieldClassificationService {
+
+ private static final String TAG = "AutofillFieldClassificationServiceImpl";
+ // TODO(b/70291841): set to false before launching
+ private static final boolean DEBUG = true;
+
+ @Nullable
+ @Override
+ public float[][] onGetScores(@Nullable String algorithmName,
+ @Nullable Bundle algorithmArgs, @NonNull List<AutofillValue> actualValues,
+ @NonNull List<String> userDataValues) {
+ if (ArrayUtils.isEmpty(actualValues) || ArrayUtils.isEmpty(userDataValues)) {
+ Log.w(TAG, "getScores(): empty currentvalues (" + actualValues + ") or userValues ("
+ + userDataValues + ")");
+ // TODO(b/70939974): add unit test
+ return null;
+ }
+ if (algorithmName != null && !algorithmName.equals(EditDistanceScorer.NAME)) {
+ Log.w(TAG, "Ignoring invalid algorithm (" + algorithmName + ") and using "
+ + EditDistanceScorer.NAME + " instead");
+ }
+
+ final String actualAlgorithmName = EditDistanceScorer.NAME;
+ final int actualValuesSize = actualValues.size();
+ final int userDataValuesSize = userDataValues.size();
+ if (DEBUG) {
+ Log.d(TAG, "getScores() will return a " + actualValuesSize + "x"
+ + userDataValuesSize + " matrix for " + actualAlgorithmName);
+ }
+ final float[][] scores = new float[actualValuesSize][userDataValuesSize];
+
+ final EditDistanceScorer algorithm = EditDistanceScorer.getInstance();
+ for (int i = 0; i < actualValuesSize; i++) {
+ for (int j = 0; j < userDataValuesSize; j++) {
+ final float score = algorithm.getScore(actualValues.get(i), userDataValues.get(j));
+ scores[i][j] = score;
+ }
+ }
+ return scores;
+ }
+}
diff --git a/android/ext/services/autofill/EditDistanceScorer.java b/android/ext/services/autofill/EditDistanceScorer.java
new file mode 100644
index 0000000..d2e804a
--- /dev/null
+++ b/android/ext/services/autofill/EditDistanceScorer.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2017 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.ext.services.autofill;
+
+import android.annotation.NonNull;
+import android.view.autofill.AutofillValue;
+
+/**
+ * Helper used to calculate the classification score between an actual {@link AutofillValue} filled
+ * by the user and the expected value predicted by an autofill service.
+ */
+// TODO(b/70291841): explain algorithm once it's fully implemented
+final class EditDistanceScorer {
+
+ private static final EditDistanceScorer sInstance = new EditDistanceScorer();
+
+ public static final String NAME = "EDIT_DISTANCE";
+
+ /**
+ * Gets the singleton instance.
+ */
+ public static EditDistanceScorer getInstance() {
+ return sInstance;
+ }
+
+ private EditDistanceScorer() {
+ }
+
+ /**
+ * Returns the classification score between an actual {@link AutofillValue} filled
+ * by the user and the expected value predicted by an autofill service.
+ *
+ * <p>A full-match is {@code 1.0} (representing 100%), a full mismatch is {@code 0.0} and
+ * partial mathces are something in between, typically using edit-distance algorithms.
+ *
+ */
+ public float getScore(@NonNull AutofillValue actualValue, @NonNull String userDataValue) {
+ if (actualValue == null || !actualValue.isText() || userDataValue == null) return 0;
+ // TODO(b/70291841): implement edit distance - currently it's returning either 0, 100%, or
+ // partial match when number of chars match
+ final String textValue = actualValue.getTextValue().toString();
+ final int total = textValue.length();
+ if (total != userDataValue.length()) return 0F;
+
+ int matches = 0;
+ for (int i = 0; i < total; i++) {
+ if (Character.toLowerCase(textValue.charAt(i)) == Character
+ .toLowerCase(userDataValue.charAt(i))) {
+ matches++;
+ }
+ }
+
+ return ((float) matches) / total;
+ }
+}
diff --git a/android/graphics/BaseCanvas.java b/android/graphics/BaseCanvas.java
index 2d8c717..627d551 100644
--- a/android/graphics/BaseCanvas.java
+++ b/android/graphics/BaseCanvas.java
@@ -22,6 +22,8 @@
import android.annotation.Size;
import android.graphics.Canvas.VertexMode;
import android.text.GraphicsOperations;
+import android.text.MeasuredParagraph;
+import android.text.MeasuredText;
import android.text.SpannableString;
import android.text.SpannedString;
import android.text.TextUtils;
@@ -453,7 +455,8 @@
throwIfHasHwBitmapInSwMode(paint);
nDrawTextRun(mNativeCanvasWrapper, text, index, count, contextIndex, contextCount,
- x, y, isRtl, paint.getNativeInstance());
+ x, y, isRtl, paint.getNativeInstance(), 0 /* measured text */,
+ 0 /* measured text offset */);
}
public void drawTextRun(@NonNull CharSequence text, int start, int end, int contextStart,
@@ -483,8 +486,20 @@
int len = end - start;
char[] buf = TemporaryBuffer.obtain(contextLen);
TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
+ long measuredTextPtr = 0;
+ int measuredTextOffset = 0;
+ if (text instanceof MeasuredText) {
+ MeasuredText mt = (MeasuredText) text;
+ int paraIndex = mt.findParaIndex(start);
+ if (end <= mt.getParagraphEnd(paraIndex)) {
+ // Only suppor the same paragraph.
+ measuredTextPtr = mt.getMeasuredParagraph(paraIndex).getNativePtr();
+ measuredTextOffset = start - mt.getParagraphStart(paraIndex);
+ }
+ }
nDrawTextRun(mNativeCanvasWrapper, buf, start - contextStart, len,
- 0, contextLen, x, y, isRtl, paint.getNativeInstance());
+ 0, contextLen, x, y, isRtl, paint.getNativeInstance(),
+ measuredTextPtr, measuredTextOffset);
TemporaryBuffer.recycle(buf);
}
}
@@ -623,7 +638,8 @@
int contextStart, int contextEnd, float x, float y, boolean isRtl, long nativePaint);
private static native void nDrawTextRun(long nativeCanvas, char[] text, int start, int count,
- int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint);
+ int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint,
+ long nativeMeasuredText, int measuredTextOffset);
private static native void nDrawTextOnPath(long nativeCanvas, char[] text, int index, int count,
long nativePath, float hOffset, float vOffset, int bidiFlags, long nativePaint);
diff --git a/android/graphics/BidiRenderer.java b/android/graphics/BidiRenderer.java
index 7b7dfa6..3dc1d41 100644
--- a/android/graphics/BidiRenderer.java
+++ b/android/graphics/BidiRenderer.java
@@ -231,6 +231,11 @@
int[] ci = gv.getGlyphCharIndices(0, ng, null);
if (advances != null) {
for (int i = 0; i < ng; i++) {
+ if (mText[ci[i]] == '\uFEFF') {
+ // Workaround for bug in JetBrains JDK
+ // where the character \uFEFF is associated a glyph with non-zero width
+ continue;
+ }
int adv_idx = advancesIndex + ci[i];
advances[adv_idx] += gv.getGlyphMetrics(i).getAdvanceX();
}
diff --git a/android/graphics/ImageDecoder.java b/android/graphics/ImageDecoder.java
index 60416a7..3de050b 100644
--- a/android/graphics/ImageDecoder.java
+++ b/android/graphics/ImageDecoder.java
@@ -16,45 +16,80 @@
package android.graphics;
+import static android.system.OsConstants.SEEK_CUR;
+import static android.system.OsConstants.SEEK_SET;
+
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.RawRes;
+import android.content.ContentResolver;
+import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.content.res.Resources;
+import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.NinePatchDrawable;
+import android.net.Uri;
+import android.util.Size;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+
+import libcore.io.IoUtils;
+import dalvik.system.CloseGuard;
import java.nio.ByteBuffer;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ArrayIndexOutOfBoundsException;
+import java.lang.AutoCloseable;
import java.lang.NullPointerException;
import java.lang.RuntimeException;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.SOURCE;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* Class for decoding images as {@link Bitmap}s or {@link Drawable}s.
- * @hide
*/
-public final class ImageDecoder {
+public final class ImageDecoder implements AutoCloseable {
/**
* Source of the encoded image data.
*/
public static abstract class Source {
+ private Source() {}
+
/* @hide */
+ @Nullable
Resources getResources() { return null; }
/* @hide */
- void close() {}
+ int getDensity() { return Bitmap.DENSITY_NONE; }
/* @hide */
- abstract ImageDecoder createImageDecoder();
+ int computeDstDensity() {
+ Resources res = getResources();
+ if (res == null) {
+ return Bitmap.getDefaultDensity();
+ }
+
+ return res.getDisplayMetrics().densityDpi;
+ }
+
+ /* @hide */
+ @NonNull
+ abstract ImageDecoder createImageDecoder() throws IOException;
};
private static class ByteArraySource extends Source {
- ByteArraySource(byte[] data, int offset, int length) {
+ ByteArraySource(@NonNull byte[] data, int offset, int length) {
mData = data;
mOffset = offset;
mLength = length;
@@ -64,19 +99,19 @@
private final int mLength;
@Override
- public ImageDecoder createImageDecoder() {
+ public ImageDecoder createImageDecoder() throws IOException {
return nCreate(mData, mOffset, mLength);
}
}
private static class ByteBufferSource extends Source {
- ByteBufferSource(ByteBuffer buffer) {
+ ByteBufferSource(@NonNull ByteBuffer buffer) {
mBuffer = buffer;
}
private final ByteBuffer mBuffer;
@Override
- public ImageDecoder createImageDecoder() {
+ public ImageDecoder createImageDecoder() throws IOException {
if (!mBuffer.isDirect() && mBuffer.hasArray()) {
int offset = mBuffer.arrayOffset() + mBuffer.position();
int length = mBuffer.limit() - mBuffer.position();
@@ -86,91 +121,230 @@
}
}
- private static class ResourceSource extends Source {
- ResourceSource(Resources res, int resId)
- throws Resources.NotFoundException {
- // Test that the resource can be found.
- InputStream is = null;
+ private static class ContentResolverSource extends Source {
+ ContentResolverSource(@NonNull ContentResolver resolver, @NonNull Uri uri) {
+ mResolver = resolver;
+ mUri = uri;
+ }
+
+ private final ContentResolver mResolver;
+ private final Uri mUri;
+
+ @Override
+ public ImageDecoder createImageDecoder() throws IOException {
+ AssetFileDescriptor assetFd = null;
try {
- is = res.openRawResource(resId);
- } finally {
- if (is != null) {
- try {
- is.close();
- } catch (IOException e) {
- }
+ if (mUri.getScheme() == ContentResolver.SCHEME_CONTENT) {
+ assetFd = mResolver.openTypedAssetFileDescriptor(mUri,
+ "image/*", null);
+ } else {
+ assetFd = mResolver.openAssetFileDescriptor(mUri, "r");
}
+ } catch (FileNotFoundException e) {
+ // Some images cannot be opened as AssetFileDescriptors (e.g.
+ // bmp, ico). Open them as InputStreams.
+ InputStream is = mResolver.openInputStream(mUri);
+ if (is == null) {
+ throw new FileNotFoundException(mUri.toString());
+ }
+
+ return createFromStream(is);
}
+ final FileDescriptor fd = assetFd.getFileDescriptor();
+ final long offset = assetFd.getStartOffset();
+
+ ImageDecoder decoder = null;
+ try {
+ try {
+ Os.lseek(fd, offset, SEEK_SET);
+ decoder = nCreate(fd);
+ } catch (ErrnoException e) {
+ decoder = createFromStream(new FileInputStream(fd));
+ }
+ } finally {
+ if (decoder == null) {
+ IoUtils.closeQuietly(assetFd);
+ } else {
+ decoder.mAssetFd = assetFd;
+ }
+ }
+ return decoder;
+ }
+ }
+
+ @NonNull
+ private static ImageDecoder createFromFile(@NonNull File file) throws IOException {
+ FileInputStream stream = new FileInputStream(file);
+ FileDescriptor fd = stream.getFD();
+ try {
+ Os.lseek(fd, 0, SEEK_CUR);
+ } catch (ErrnoException e) {
+ return createFromStream(stream);
+ }
+
+ ImageDecoder decoder = null;
+ try {
+ decoder = nCreate(fd);
+ } finally {
+ if (decoder == null) {
+ IoUtils.closeQuietly(stream);
+ } else {
+ decoder.mInputStream = stream;
+ }
+ }
+ return decoder;
+ }
+
+ @NonNull
+ private static ImageDecoder createFromStream(@NonNull InputStream is) throws IOException {
+ // Arbitrary size matches BitmapFactory.
+ byte[] storage = new byte[16 * 1024];
+ ImageDecoder decoder = null;
+ try {
+ decoder = nCreate(is, storage);
+ } finally {
+ if (decoder == null) {
+ IoUtils.closeQuietly(is);
+ } else {
+ decoder.mInputStream = is;
+ decoder.mTempStorage = storage;
+ }
+ }
+
+ return decoder;
+ }
+
+ private static class InputStreamSource extends Source {
+ InputStreamSource(Resources res, InputStream is, int inputDensity) {
+ if (is == null) {
+ throw new IllegalArgumentException("The InputStream cannot be null");
+ }
mResources = res;
- mResId = resId;
+ mInputStream = is;
+ mInputDensity = res != null ? inputDensity : Bitmap.DENSITY_NONE;
}
final Resources mResources;
- final int mResId;
- // This is just stored here in order to keep the underlying Asset
- // alive. FIXME: Can I access the Asset (and keep it alive) without
- // this object?
InputStream mInputStream;
+ final int mInputDensity;
@Override
public Resources getResources() { return mResources; }
@Override
- public ImageDecoder createImageDecoder() {
- // FIXME: Can I bypass creating the stream?
- try {
- mInputStream = mResources.openRawResource(mResId);
- } catch (Resources.NotFoundException e) {
- // This should never happen, since we already tested in the
- // constructor.
- }
- if (!(mInputStream instanceof AssetManager.AssetInputStream)) {
- // This should never happen.
- throw new RuntimeException("Resource is not an asset?");
- }
- long asset = ((AssetManager.AssetInputStream) mInputStream).getNativeAsset();
- return nCreate(asset);
- }
+ public int getDensity() { return mInputDensity; }
@Override
- public void close() {
- try {
- mInputStream.close();
- } catch (IOException e) {
- } finally {
+ public ImageDecoder createImageDecoder() throws IOException {
+
+ synchronized (this) {
+ if (mInputStream == null) {
+ throw new IOException("Cannot reuse InputStreamSource");
+ }
+ InputStream is = mInputStream;
mInputStream = null;
+ return createFromStream(is);
}
}
}
+ private static class ResourceSource extends Source {
+ ResourceSource(@NonNull Resources res, int resId) {
+ mResources = res;
+ mResId = resId;
+ mResDensity = Bitmap.DENSITY_NONE;
+ }
+
+ final Resources mResources;
+ final int mResId;
+ int mResDensity;
+
+ @Override
+ public Resources getResources() { return mResources; }
+
+ @Override
+ public int getDensity() { return mResDensity; }
+
+ @Override
+ public ImageDecoder createImageDecoder() throws IOException {
+ // This is just used in order to access the underlying Asset and
+ // keep it alive. FIXME: Can we skip creating this object?
+ InputStream is = null;
+ ImageDecoder decoder = null;
+ TypedValue value = new TypedValue();
+ try {
+ is = mResources.openRawResource(mResId, value);
+
+ if (value.density == TypedValue.DENSITY_DEFAULT) {
+ mResDensity = DisplayMetrics.DENSITY_DEFAULT;
+ } else if (value.density != TypedValue.DENSITY_NONE) {
+ mResDensity = value.density;
+ }
+
+ if (!(is instanceof AssetManager.AssetInputStream)) {
+ // This should never happen.
+ throw new RuntimeException("Resource is not an asset?");
+ }
+ long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
+ decoder = nCreate(asset);
+ } finally {
+ if (decoder == null) {
+ IoUtils.closeQuietly(is);
+ } else {
+ decoder.mInputStream = is;
+ }
+ }
+ return decoder;
+ }
+ }
+
+ private static class FileSource extends Source {
+ FileSource(@NonNull File file) {
+ mFile = file;
+ }
+
+ private final File mFile;
+
+ @Override
+ public ImageDecoder createImageDecoder() throws IOException {
+ return createFromFile(mFile);
+ }
+ }
+
/**
* Contains information about the encoded image.
*/
public static class ImageInfo {
- public final int width;
- public final int height;
- // TODO?: Add more info? mimetype, ninepatch etc?
+ private final Size mSize;
+ private ImageDecoder mDecoder;
- ImageInfo(int width, int height) {
- this.width = width;
- this.height = height;
+ private ImageInfo(@NonNull ImageDecoder decoder) {
+ mSize = new Size(decoder.mWidth, decoder.mHeight);
+ mDecoder = decoder;
+ }
+
+ /**
+ * Size of the image, without scaling or cropping.
+ */
+ @NonNull
+ public Size getSize() {
+ return mSize;
+ }
+
+ /**
+ * The mimeType of the image.
+ */
+ @NonNull
+ public String getMimeType() {
+ return mDecoder.getMimeType();
}
};
/**
- * Used if the provided data is incomplete.
- *
- * There may be a partial image to display.
+ * Thrown if the provided data is incomplete.
*/
- public class IncompleteException extends Exception {};
-
- /**
- * Used if the provided data is corrupt.
- *
- * There may be a partial image to display.
- */
- public class CorruptException extends Exception {};
+ public static class IncompleteException extends IOException {};
/**
* Optional listener supplied to {@link #decodeDrawable} or
@@ -180,78 +354,145 @@
/**
* Called when the header is decoded and the size is known.
*
- * @param info Information about the encoded image.
* @param decoder allows changing the default settings of the decode.
+ * @param info Information about the encoded image.
+ * @param source that created the decoder.
*/
- public void onHeaderDecoded(ImageInfo info, ImageDecoder decoder);
+ public void onHeaderDecoded(@NonNull ImageDecoder decoder,
+ @NonNull ImageInfo info, @NonNull Source source);
};
/**
- * Optional listener supplied to the ImageDecoder.
+ * An Exception was thrown reading the {@link Source}.
*/
- public static interface OnExceptionListener {
+ public static final int ERROR_SOURCE_EXCEPTION = 1;
+
+ /**
+ * The encoded data was incomplete.
+ */
+ public static final int ERROR_SOURCE_INCOMPLETE = 2;
+
+ /**
+ * The encoded data contained an error.
+ */
+ public static final int ERROR_SOURCE_ERROR = 3;
+
+ @Retention(SOURCE)
+ @IntDef({ ERROR_SOURCE_EXCEPTION, ERROR_SOURCE_INCOMPLETE, ERROR_SOURCE_ERROR })
+ public @interface Error {};
+
+ /**
+ * Optional listener supplied to the ImageDecoder.
+ *
+ * Without this listener, errors will throw {@link java.io.IOException}.
+ */
+ public static interface OnPartialImageListener {
/**
- * Called when there is a problem in the stream or in the data.
- * FIXME: Or do not allow streams?
- * FIXME: Report how much of the image has been decoded?
+ * Called when there is only a partial image to display.
*
- * @param e Exception containing information about the error.
- * @return True to create and return a {@link Drawable}/
- * {@link Bitmap} with partial data. False to return
- * {@code null}. True is the default.
+ * If decoding is interrupted after having decoded a partial image,
+ * this listener lets the client know that and allows them to
+ * optionally finish the rest of the decode/creation process to create
+ * a partial {@link Drawable}/{@link Bitmap}.
+ *
+ * @param error indicating what interrupted the decode.
+ * @param source that had the error.
+ * @return True to create and return a {@link Drawable}/{@link Bitmap}
+ * with partial data. False (which is the default) to abort the
+ * decode and throw {@link java.io.IOException}.
*/
- public boolean onException(Exception e);
+ public boolean onPartialImage(@Error int error, @NonNull Source source);
};
// Fields
- private long mNativePtr;
- private final int mWidth;
- private final int mHeight;
+ private long mNativePtr;
+ private final int mWidth;
+ private final int mHeight;
+ private final boolean mAnimated;
private int mDesiredWidth;
private int mDesiredHeight;
- private int mAllocator = DEFAULT_ALLOCATOR;
+ private int mAllocator = ALLOCATOR_DEFAULT;
private boolean mRequireUnpremultiplied = false;
private boolean mMutable = false;
private boolean mPreferRamOverQuality = false;
private boolean mAsAlphaMask = false;
private Rect mCropRect;
+ private Source mSource;
- private PostProcess mPostProcess;
- private OnExceptionListener mOnExceptionListener;
+ private PostProcessor mPostProcessor;
+ private OnPartialImageListener mOnPartialImageListener;
+ // Objects for interacting with the input.
+ private InputStream mInputStream;
+ private byte[] mTempStorage;
+ private AssetFileDescriptor mAssetFd;
+ private final AtomicBoolean mClosed = new AtomicBoolean();
+ private final CloseGuard mCloseGuard = CloseGuard.get();
/**
- * Private constructor called by JNI. {@link #recycle} must be
+ * Private constructor called by JNI. {@link #close} must be
* called after decoding to delete native resources.
*/
@SuppressWarnings("unused")
- private ImageDecoder(long nativePtr, int width, int height) {
+ private ImageDecoder(long nativePtr, int width, int height,
+ boolean animated) {
mNativePtr = nativePtr;
mWidth = width;
mHeight = height;
mDesiredWidth = width;
mDesiredHeight = height;
+ mAnimated = animated;
+ mCloseGuard.open("close");
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+
+ close();
+ } finally {
+ super.finalize();
+ }
}
/**
* Create a new {@link Source} from an asset.
+ * @hide
*
* @param res the {@link Resources} object containing the image data.
* @param resId resource ID of the image data.
* // FIXME: Can be an @DrawableRes?
* @return a new Source object, which can be passed to
* {@link #decodeDrawable} or {@link #decodeBitmap}.
- * @throws Resources.NotFoundException if the asset does not exist.
*/
+ @NonNull
public static Source createSource(@NonNull Resources res, @RawRes int resId)
- throws Resources.NotFoundException {
+ {
return new ResourceSource(res, resId);
}
/**
+ * Create a new {@link Source} from a {@link android.net.Uri}.
+ *
+ * @param cr to retrieve from.
+ * @param uri of the image file.
+ * @return a new Source object, which can be passed to
+ * {@link #decodeDrawable} or {@link #decodeBitmap}.
+ */
+ @NonNull
+ public static Source createSource(@NonNull ContentResolver cr,
+ @NonNull Uri uri) {
+ return new ContentResolverSource(cr, uri);
+ }
+
+ /**
* Create a new {@link Source} from a byte array.
+ *
* @param data byte array of compressed image data.
* @param offset offset into data for where the decoder should begin
* parsing.
@@ -259,8 +500,9 @@
* @throws NullPointerException if data is null.
* @throws ArrayIndexOutOfBoundsException if offset and length are
* not within data.
+ * @hide
*/
- // TODO: Overloads that don't use offset, length
+ @NonNull
public static Source createSource(@NonNull byte[] data, int offset,
int length) throws ArrayIndexOutOfBoundsException {
if (data == null) {
@@ -275,39 +517,75 @@
}
/**
+ * See {@link #createSource(byte[], int, int).
+ * @hide
+ */
+ @NonNull
+ public static Source createSource(@NonNull byte[] data) {
+ return createSource(data, 0, data.length);
+ }
+
+ /**
* Create a new {@link Source} from a {@link java.nio.ByteBuffer}.
*
- * The returned {@link Source} effectively takes ownership of the
+ * <p>The returned {@link Source} effectively takes ownership of the
* {@link java.nio.ByteBuffer}; i.e. no other code should modify it after
- * this call.
+ * this call.</p>
*
- * Decoding will start from {@link java.nio.ByteBuffer#position()}.
+ * Decoding will start from {@link java.nio.ByteBuffer#position()}. The
+ * position after decoding is undefined.
*/
- public static Source createSource(ByteBuffer buffer) {
+ @NonNull
+ public static Source createSource(@NonNull ByteBuffer buffer) {
return new ByteBufferSource(buffer);
}
/**
+ * Internal API used to generate bitmaps for use by Drawables (i.e. BitmapDrawable)
+ * @hide
+ */
+ public static Source createSource(Resources res, InputStream is) {
+ return new InputStreamSource(res, is, Bitmap.getDefaultDensity());
+ }
+
+ /**
+ * Internal API used to generate bitmaps for use by Drawables (i.e. BitmapDrawable)
+ * @hide
+ */
+ public static Source createSource(Resources res, InputStream is, int density) {
+ return new InputStreamSource(res, is, density);
+ }
+
+ /**
+ * Create a new {@link Source} from a {@link java.io.File}.
+ */
+ @NonNull
+ public static Source createSource(@NonNull File file) {
+ return new FileSource(file);
+ }
+
+ /**
* Return the width and height of a given sample size.
*
- * This takes an input that functions like
+ * <p>This takes an input that functions like
* {@link BitmapFactory.Options#inSampleSize}. It returns a width and
* height that can be acheived by sampling the encoded image. Other widths
* and heights may be supported, but will require an additional (internal)
* scaling step. Such internal scaling is *not* supported with
- * {@link #requireUnpremultiplied}.
+ * {@link #setRequireUnpremultiplied} set to {@code true}.</p>
*
* @param sampleSize Sampling rate of the encoded image.
- * @return Point {@link Point#x} and {@link Point#y} correspond to the
- * width and height after sampling.
+ * @return {@link android.util.Size} of the width and height after
+ * sampling.
*/
- public Point getSampledSize(int sampleSize) {
+ @NonNull
+ public Size getSampledSize(int sampleSize) {
if (sampleSize <= 0) {
throw new IllegalArgumentException("sampleSize must be positive! "
+ "provided " + sampleSize);
}
if (mNativePtr == 0) {
- throw new IllegalStateException("ImageDecoder is recycled!");
+ throw new IllegalStateException("ImageDecoder is closed!");
}
return nGetSampledSize(mNativePtr, sampleSize);
@@ -320,7 +598,7 @@
* @param width must be greater than 0.
* @param height must be greater than 0.
*/
- public void resize(int width, int height) {
+ public void setResize(int width, int height) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Dimensions must be positive! "
+ "provided (" + width + ", " + height + ")");
@@ -333,14 +611,18 @@
/**
* Resize based on a sample size.
*
- * This has the same effect as passing the result of
- * {@link #getSampledSize} to {@link #resize(int, int)}.
+ * <p>This has the same effect as passing the result of
+ * {@link #getSampledSize} to {@link #setResize(int, int)}.</p>
*
* @param sampleSize Sampling rate of the encoded image.
*/
- public void resize(int sampleSize) {
- Point dimensions = this.getSampledSize(sampleSize);
- this.resize(dimensions.x, dimensions.y);
+ public void setResize(int sampleSize) {
+ Size size = this.getSampledSize(sampleSize);
+ this.setResize(size.getWidth(), size.getHeight());
+ }
+
+ private boolean requestedResize() {
+ return mWidth != mDesiredWidth || mHeight != mDesiredHeight;
}
// These need to stay in sync with ImageDecoder.cpp's Allocator enum.
@@ -352,7 +634,7 @@
* switch to software when HARDWARE is incompatible, e.g.
* {@link #setMutable}, {@link #setAsAlphaMask}.
*/
- public static final int DEFAULT_ALLOCATOR = 0;
+ public static final int ALLOCATOR_DEFAULT = 0;
/**
* Use a software allocation for the pixel memory.
@@ -360,28 +642,29 @@
* Useful for drawing to a software {@link Canvas} or for
* accessing the pixels on the final output.
*/
- public static final int SOFTWARE_ALLOCATOR = 1;
+ public static final int ALLOCATOR_SOFTWARE = 1;
/**
* Use shared memory for the pixel memory.
*
* Useful for sharing across processes.
*/
- public static final int SHARED_MEMORY_ALLOCATOR = 2;
+ public static final int ALLOCATOR_SHARED_MEMORY = 2;
/**
* Require a {@link Bitmap.Config#HARDWARE} {@link Bitmap}.
*
- * This will throw an {@link java.lang.IllegalStateException} when combined
- * with incompatible options, like {@link #setMutable} or
- * {@link #setAsAlphaMask}.
+ * When this is combined with incompatible options, like
+ * {@link #setMutable} or {@link #setAsAlphaMask}, {@link #decodeDrawable}
+ * / {@link #decodeBitmap} will throw an
+ * {@link java.lang.IllegalStateException}.
*/
- public static final int HARDWARE_ALLOCATOR = 3;
+ public static final int ALLOCATOR_HARDWARE = 3;
/** @hide **/
@Retention(SOURCE)
- @IntDef({ DEFAULT_ALLOCATOR, SOFTWARE_ALLOCATOR, SHARED_MEMORY_ALLOCATOR,
- HARDWARE_ALLOCATOR })
+ @IntDef({ ALLOCATOR_DEFAULT, ALLOCATOR_SOFTWARE, ALLOCATOR_SHARED_MEMORY,
+ ALLOCATOR_HARDWARE })
public @interface Allocator {};
/**
@@ -389,140 +672,147 @@
*
* This is ignored for animated drawables.
*
- * TODO: Allow accessing the backing from the Bitmap.
- *
* @param allocator Type of allocator to use.
*/
public void setAllocator(@Allocator int allocator) {
- if (allocator < DEFAULT_ALLOCATOR || allocator > HARDWARE_ALLOCATOR) {
+ if (allocator < ALLOCATOR_DEFAULT || allocator > ALLOCATOR_HARDWARE) {
throw new IllegalArgumentException("invalid allocator " + allocator);
}
mAllocator = allocator;
}
/**
- * Create a {@link Bitmap} with unpremultiplied pixels.
+ * Specify whether the {@link Bitmap} should have unpremultiplied pixels.
*
* By default, ImageDecoder will create a {@link Bitmap} with
* premultiplied pixels, which is required for drawing with the
* {@link android.view.View} system (i.e. to a {@link Canvas}). Calling
- * this method will result in {@link #decodeBitmap} returning a
- * {@link Bitmap} with unpremultiplied pixels. See
- * {@link Bitmap#isPremultiplied}. Incompatible with
+ * this method with a value of {@code true} will result in
+ * {@link #decodeBitmap} returning a {@link Bitmap} with unpremultiplied
+ * pixels. See {@link Bitmap#isPremultiplied}. This is incompatible with
* {@link #decodeDrawable}; attempting to decode an unpremultiplied
* {@link Drawable} will throw an {@link java.lang.IllegalStateException}.
*/
- public void requireUnpremultiplied() {
- mRequireUnpremultiplied = true;
+ public void setRequireUnpremultiplied(boolean requireUnpremultiplied) {
+ mRequireUnpremultiplied = requireUnpremultiplied;
}
/**
* Modify the image after decoding and scaling.
*
- * This allows adding effects prior to returning a {@link Drawable} or
+ * <p>This allows adding effects prior to returning a {@link Drawable} or
* {@link Bitmap}. For a {@code Drawable} or an immutable {@code Bitmap},
- * this is the only way to process the image after decoding.
+ * this is the only way to process the image after decoding.</p>
*
- * If set on a nine-patch image, the nine-patch data is ignored.
+ * <p>If set on a nine-patch image, the nine-patch data is ignored.</p>
*
- * For an animated image, the drawing commands drawn on the {@link Canvas}
- * will be recorded immediately and then applied to each frame.
+ * <p>For an animated image, the drawing commands drawn on the
+ * {@link Canvas} will be recorded immediately and then applied to each
+ * frame.</p>
*/
- public void setPostProcess(PostProcess p) {
- mPostProcess = p;
+ public void setPostProcessor(@Nullable PostProcessor p) {
+ mPostProcessor = p;
}
/**
- * Set (replace) the {@link OnExceptionListener} on this object.
+ * Set (replace) the {@link OnPartialImageListener} on this object.
*
* Will be called if there is an error in the input. Without one, a
* partial {@link Bitmap} will be created.
*/
- public void setOnExceptionListener(OnExceptionListener l) {
- mOnExceptionListener = l;
+ public void setOnPartialImageListener(@Nullable OnPartialImageListener l) {
+ mOnPartialImageListener = l;
}
/**
* Crop the output to {@code subset} of the (possibly) scaled image.
*
- * {@code subset} must be contained within the size set by {@link #resize}
- * or the bounds of the image if resize was not called. Otherwise an
- * {@link IllegalStateException} will be thrown.
+ * <p>{@code subset} must be contained within the size set by
+ * {@link #setResize} or the bounds of the image if setResize was not
+ * called. Otherwise an {@link IllegalStateException} will be thrown by
+ * {@link #decodeDrawable}/{@link #decodeBitmap}.</p>
*
- * NOT intended as a replacement for
+ * <p>NOT intended as a replacement for
* {@link BitmapRegionDecoder#decodeRegion}. This supports all formats,
- * but merely crops the output.
+ * but merely crops the output.</p>
*/
- public void crop(Rect subset) {
+ public void setCrop(@Nullable Rect subset) {
mCropRect = subset;
}
/**
- * Create a mutable {@link Bitmap}.
+ * Specify whether the {@link Bitmap} should be mutable.
*
- * By default, a {@link Bitmap} created will be immutable, but that can be
- * changed with this call.
+ * <p>By default, a {@link Bitmap} created will be immutable, but that can
+ * be changed with this call.</p>
*
- * Incompatible with {@link #HARDWARE_ALLOCATOR}, because
- * {@link Bitmap.Config#HARDWARE} Bitmaps cannot be mutable. Attempting to
- * combine them will throw an {@link java.lang.IllegalStateException}.
+ * <p>Mutable Bitmaps are incompatible with {@link #ALLOCATOR_HARDWARE},
+ * because {@link Bitmap.Config#HARDWARE} Bitmaps cannot be mutable.
+ * Attempting to combine them will throw an
+ * {@link java.lang.IllegalStateException}.</p>
*
- * Incompatible with {@link #decodeDrawable}, which would require
- * retrieving the Bitmap from the returned Drawable in order to modify.
- * Attempting to decode a mutable {@link Drawable} will throw an
- * {@link java.lang.IllegalStateException}
+ * <p>Mutable Bitmaps are also incompatible with {@link #decodeDrawable},
+ * which would require retrieving the Bitmap from the returned Drawable in
+ * order to modify. Attempting to decode a mutable {@link Drawable} will
+ * throw an {@link java.lang.IllegalStateException}.</p>
*/
- public void setMutable() {
- mMutable = true;
+ public void setMutable(boolean mutable) {
+ mMutable = mutable;
}
/**
- * Potentially save RAM at the expense of quality.
+ * Specify whether to potentially save RAM at the expense of quality.
*
- * This may result in a {@link Bitmap} with a denser {@link Bitmap.Config},
- * depending on the image. For example, for an opaque {@link Bitmap}, this
- * may result in a {@link Bitmap.Config} with no alpha information.
+ * Setting this to {@code true} may result in a {@link Bitmap} with a
+ * denser {@link Bitmap.Config}, depending on the image. For example, for
+ * an opaque {@link Bitmap}, this may result in a {@link Bitmap.Config}
+ * with no alpha information.
*/
- public void setPreferRamOverQuality() {
- mPreferRamOverQuality = true;
+ public void setPreferRamOverQuality(boolean preferRamOverQuality) {
+ mPreferRamOverQuality = preferRamOverQuality;
}
/**
- * Potentially treat the output as an alpha mask.
+ * Specify whether to potentially treat the output as an alpha mask.
*
- * If the image is encoded in a format with only one channel, treat that
- * channel as alpha. Otherwise this call has no effect.
+ * <p>If this is set to {@code true} and the image is encoded in a format
+ * with only one channel, treat that channel as alpha. Otherwise this call has
+ * no effect.</p>
*
- * Incompatible with {@link #HARDWARE_ALLOCATOR}. Trying to combine them
- * will throw an {@link java.lang.IllegalStateException}.
+ * <p>setAsAlphaMask is incompatible with {@link #ALLOCATOR_HARDWARE}. Trying to
+ * combine them will result in {@link #decodeDrawable}/
+ * {@link #decodeBitmap} throwing an
+ * {@link java.lang.IllegalStateException}.</p>
*/
- public void setAsAlphaMask() {
- mAsAlphaMask = true;
+ public void setAsAlphaMask(boolean asAlphaMask) {
+ mAsAlphaMask = asAlphaMask;
}
- /**
- * Clean up resources.
- *
- * ImageDecoder has a private constructor, and will always be recycled
- * by decodeDrawable or decodeBitmap which creates it, so there is no
- * need for a finalizer.
- */
- private void recycle() {
- if (mNativePtr == 0) {
+ @Override
+ public void close() {
+ mCloseGuard.close();
+ if (!mClosed.compareAndSet(false, true)) {
return;
}
- nRecycle(mNativePtr);
+ nClose(mNativePtr);
mNativePtr = 0;
+
+ IoUtils.closeQuietly(mInputStream);
+ IoUtils.closeQuietly(mAssetFd);
+
+ mInputStream = null;
+ mAssetFd = null;
+ mTempStorage = null;
}
private void checkState() {
if (mNativePtr == 0) {
- throw new IllegalStateException("Cannot reuse ImageDecoder.Source!");
+ throw new IllegalStateException("Cannot use closed ImageDecoder!");
}
checkSubset(mDesiredWidth, mDesiredHeight, mCropRect);
- if (mAllocator == HARDWARE_ALLOCATOR) {
+ if (mAllocator == ALLOCATOR_HARDWARE) {
if (mMutable) {
throw new IllegalStateException("Cannot make mutable HARDWARE Bitmap!");
}
@@ -531,7 +821,7 @@
}
}
- if (mPostProcess != null && mRequireUnpremultiplied) {
+ if (mPostProcessor != null && mRequireUnpremultiplied) {
throw new IllegalStateException("Cannot draw to unpremultiplied pixels!");
}
}
@@ -546,54 +836,88 @@
}
}
- /**
- * Create a {@link Drawable}.
- */
- public static Drawable decodeDrawable(Source src, OnHeaderDecodedListener listener) {
- ImageDecoder decoder = src.createImageDecoder();
- if (decoder == null) {
- return null;
- }
+ @NonNull
+ private Bitmap decodeBitmap() throws IOException {
+ checkState();
+ // nDecodeBitmap calls onPartialImage only if mOnPartialImageListener
+ // exists
+ ImageDecoder partialImagePtr = mOnPartialImageListener == null ? null : this;
+ // nDecodeBitmap calls postProcessAndRelease only if mPostProcessor
+ // exists.
+ ImageDecoder postProcessPtr = mPostProcessor == null ? null : this;
+ return nDecodeBitmap(mNativePtr, partialImagePtr,
+ postProcessPtr, mDesiredWidth, mDesiredHeight, mCropRect,
+ mMutable, mAllocator, mRequireUnpremultiplied,
+ mPreferRamOverQuality, mAsAlphaMask);
+ }
+
+ private void callHeaderDecoded(@Nullable OnHeaderDecodedListener listener,
+ @NonNull Source src) {
if (listener != null) {
- ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight);
- listener.onHeaderDecoded(info, decoder);
- }
-
- decoder.checkState();
-
- if (decoder.mRequireUnpremultiplied) {
- // Though this could be supported (ignored) for opaque images, it
- // seems better to always report this error.
- throw new IllegalStateException("Cannot decode a Drawable with" +
- " unpremultiplied pixels!");
- }
-
- if (decoder.mMutable) {
- throw new IllegalStateException("Cannot decode a mutable Drawable!");
- }
-
- try {
- Bitmap bm = nDecodeBitmap(decoder.mNativePtr,
- decoder.mOnExceptionListener,
- decoder.mPostProcess,
- decoder.mDesiredWidth, decoder.mDesiredHeight,
- decoder.mCropRect,
- false, // decoder.mMutable
- decoder.mAllocator,
- false, // decoder.mRequireUnpremultiplied
- decoder.mPreferRamOverQuality,
- decoder.mAsAlphaMask
- );
- if (bm == null) {
- return null;
+ ImageInfo info = new ImageInfo(this);
+ try {
+ listener.onHeaderDecoded(this, info, src);
+ } finally {
+ info.mDecoder = null;
}
+ }
+ }
+
+ /**
+ * Create a {@link Drawable} from a {@code Source}.
+ *
+ * @param src representing the encoded image.
+ * @param listener for learning the {@link ImageInfo} and changing any
+ * default settings on the {@code ImageDecoder}. If not {@code null},
+ * this will be called on the same thread as {@code decodeDrawable}
+ * before that method returns.
+ * @return Drawable for displaying the image.
+ * @throws IOException if {@code src} is not found, is an unsupported
+ * format, or cannot be decoded for any reason.
+ */
+ @NonNull
+ public static Drawable decodeDrawable(@NonNull Source src,
+ @Nullable OnHeaderDecodedListener listener) throws IOException {
+ try (ImageDecoder decoder = src.createImageDecoder()) {
+ decoder.mSource = src;
+ decoder.callHeaderDecoded(listener, src);
+
+ if (decoder.mRequireUnpremultiplied) {
+ // Though this could be supported (ignored) for opaque images,
+ // it seems better to always report this error.
+ throw new IllegalStateException("Cannot decode a Drawable " +
+ "with unpremultiplied pixels!");
+ }
+
+ if (decoder.mMutable) {
+ throw new IllegalStateException("Cannot decode a mutable " +
+ "Drawable!");
+ }
+
+ // this call potentially manipulates the decoder so it must be performed prior to
+ // decoding the bitmap and after decode set the density on the resulting bitmap
+ final int srcDensity = computeDensity(src, decoder);
+ if (decoder.mAnimated) {
+ // AnimatedImageDrawable calls postProcessAndRelease only if
+ // mPostProcessor exists.
+ ImageDecoder postProcessPtr = decoder.mPostProcessor == null ?
+ null : decoder;
+ Drawable d = new AnimatedImageDrawable(decoder.mNativePtr,
+ postProcessPtr, decoder.mDesiredWidth,
+ decoder.mDesiredHeight, srcDensity,
+ src.computeDstDensity(), decoder.mCropRect,
+ decoder.mInputStream, decoder.mAssetFd);
+ // d has taken ownership of these objects.
+ decoder.mInputStream = null;
+ decoder.mAssetFd = null;
+ return d;
+ }
+
+ Bitmap bm = decoder.decodeBitmap();
+ bm.setDensity(srcDensity);
Resources res = src.getResources();
- if (res == null) {
- bm.setDensity(Bitmap.DENSITY_NONE);
- }
-
byte[] np = bm.getNinePatchChunk();
if (np != null && NinePatch.isNinePatchChunk(np)) {
Rect opticalInsets = new Rect();
@@ -604,62 +928,134 @@
opticalInsets, null);
}
- // TODO: Handle animation.
return new BitmapDrawable(res, bm);
- } finally {
- decoder.recycle();
- src.close();
}
}
/**
- * Create a {@link Bitmap}.
+ * See {@link #decodeDrawable(Source, OnHeaderDecodedListener)}.
*/
- public static Bitmap decodeBitmap(Source src, OnHeaderDecodedListener listener) {
- ImageDecoder decoder = src.createImageDecoder();
- if (decoder == null) {
- return null;
- }
+ @NonNull
+ public static Drawable decodeDrawable(@NonNull Source src)
+ throws IOException {
+ return decodeDrawable(src, null);
+ }
- if (listener != null) {
- ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight);
- listener.onHeaderDecoded(info, decoder);
- }
+ /**
+ * Create a {@link Bitmap} from a {@code Source}.
+ *
+ * @param src representing the encoded image.
+ * @param listener for learning the {@link ImageInfo} and changing any
+ * default settings on the {@code ImageDecoder}. If not {@code null},
+ * this will be called on the same thread as {@code decodeBitmap}
+ * before that method returns.
+ * @return Bitmap containing the image.
+ * @throws IOException if {@code src} is not found, is an unsupported
+ * format, or cannot be decoded for any reason.
+ */
+ @NonNull
+ public static Bitmap decodeBitmap(@NonNull Source src,
+ @Nullable OnHeaderDecodedListener listener) throws IOException {
+ try (ImageDecoder decoder = src.createImageDecoder()) {
+ decoder.mSource = src;
+ decoder.callHeaderDecoded(listener, src);
- decoder.checkState();
-
- try {
- return nDecodeBitmap(decoder.mNativePtr,
- decoder.mOnExceptionListener,
- decoder.mPostProcess,
- decoder.mDesiredWidth, decoder.mDesiredHeight,
- decoder.mCropRect,
- decoder.mMutable,
- decoder.mAllocator,
- decoder.mRequireUnpremultiplied,
- decoder.mPreferRamOverQuality,
- decoder.mAsAlphaMask);
- } finally {
- decoder.recycle();
- src.close();
+ // this call potentially manipulates the decoder so it must be performed prior to
+ // decoding the bitmap
+ final int srcDensity = computeDensity(src, decoder);
+ Bitmap bm = decoder.decodeBitmap();
+ bm.setDensity(srcDensity);
+ return bm;
}
}
- private static native ImageDecoder nCreate(long asset);
+ // This method may modify the decoder so it must be called prior to performing the decode
+ private static int computeDensity(@NonNull Source src, @NonNull ImageDecoder decoder) {
+ // if the caller changed the size then we treat the density as unknown
+ if (decoder.requestedResize()) {
+ return Bitmap.DENSITY_NONE;
+ }
+
+ // Special stuff for compatibility mode: if the target density is not
+ // the same as the display density, but the resource -is- the same as
+ // the display density, then don't scale it down to the target density.
+ // This allows us to load the system's density-correct resources into
+ // an application in compatibility mode, without scaling those down
+ // to the compatibility density only to have them scaled back up when
+ // drawn to the screen.
+ Resources res = src.getResources();
+ final int srcDensity = src.getDensity();
+ if (res != null && res.getDisplayMetrics().noncompatDensityDpi == srcDensity) {
+ return srcDensity;
+ }
+
+ // downscale the bitmap if the asset has a higher density than the default
+ final int dstDensity = src.computeDstDensity();
+ if (srcDensity != Bitmap.DENSITY_NONE && srcDensity > dstDensity) {
+ float scale = (float) dstDensity / srcDensity;
+ int scaledWidth = (int) (decoder.mWidth * scale + 0.5f);
+ int scaledHeight = (int) (decoder.mHeight * scale + 0.5f);
+ decoder.setResize(scaledWidth, scaledHeight);
+ return dstDensity;
+ }
+
+ return srcDensity;
+ }
+
+ @NonNull
+ private String getMimeType() {
+ return nGetMimeType(mNativePtr);
+ }
+
+ /**
+ * See {@link #decodeBitmap(Source, OnHeaderDecodedListener)}.
+ */
+ @NonNull
+ public static Bitmap decodeBitmap(@NonNull Source src) throws IOException {
+ return decodeBitmap(src, null);
+ }
+
+ /**
+ * Private method called by JNI.
+ */
+ @SuppressWarnings("unused")
+ private int postProcessAndRelease(@NonNull Canvas canvas) {
+ try {
+ return mPostProcessor.onPostProcess(canvas);
+ } finally {
+ canvas.release();
+ }
+ }
+
+ /**
+ * Private method called by JNI.
+ */
+ @SuppressWarnings("unused")
+ private boolean onPartialImage(@Error int error) {
+ return mOnPartialImageListener.onPartialImage(error, mSource);
+ }
+
+ private static native ImageDecoder nCreate(long asset) throws IOException;
private static native ImageDecoder nCreate(ByteBuffer buffer,
int position,
- int limit);
+ int limit) throws IOException;
private static native ImageDecoder nCreate(byte[] data, int offset,
- int length);
+ int length) throws IOException;
+ private static native ImageDecoder nCreate(InputStream is, byte[] storage);
+ // The fd must be seekable.
+ private static native ImageDecoder nCreate(FileDescriptor fd) throws IOException;
+ @NonNull
private static native Bitmap nDecodeBitmap(long nativePtr,
- OnExceptionListener listener,
- PostProcess postProcess,
+ @Nullable ImageDecoder partialImageListener,
+ @Nullable ImageDecoder postProcessor,
int width, int height,
- Rect cropRect, boolean mutable,
+ @Nullable Rect cropRect, boolean mutable,
int allocator, boolean requireUnpremul,
- boolean preferRamOverQuality, boolean asAlphaMask);
- private static native Point nGetSampledSize(long nativePtr,
- int sampleSize);
- private static native void nGetPadding(long nativePtr, Rect outRect);
- private static native void nRecycle(long nativePtr);
+ boolean preferRamOverQuality, boolean asAlphaMask)
+ throws IOException;
+ private static native Size nGetSampledSize(long nativePtr,
+ int sampleSize);
+ private static native void nGetPadding(long nativePtr, @NonNull Rect outRect);
+ private static native void nClose(long nativePtr);
+ private static native String nGetMimeType(long nativePtr);
}
diff --git a/android/graphics/ImageDecoder_Delegate.java b/android/graphics/ImageDecoder_Delegate.java
new file mode 100644
index 0000000..d9fe9bf
--- /dev/null
+++ b/android/graphics/ImageDecoder_Delegate.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2018 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.graphics;
+
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.ImageDecoder.InputStreamSource;
+import android.graphics.ImageDecoder.OnHeaderDecodedListener;
+import android.graphics.ImageDecoder.Source;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
+
+import java.io.IOException;
+
+public class ImageDecoder_Delegate {
+ @LayoutlibDelegate
+ static Bitmap decodeBitmap(@NonNull Source src, @Nullable OnHeaderDecodedListener listener)
+ throws IOException {
+ TypedValue value = new TypedValue();
+ value.density = src.getDensity();
+ return BitmapFactory.decodeResourceStream(src.getResources(), value,
+ ((InputStreamSource) src).mInputStream, null, null);
+ }
+
+ @LayoutlibDelegate
+ static Bitmap decodeBitmap(@NonNull Source src) throws IOException {
+ return decodeBitmap(src, null);
+ }
+
+ @LayoutlibDelegate
+ static Bitmap decodeBitmap(ImageDecoder thisDecoder) {
+ return null;
+ }
+
+ @LayoutlibDelegate
+ static Drawable decodeDrawable(@NonNull Source src, @Nullable OnHeaderDecodedListener listener)
+ throws IOException {
+ Bitmap bitmap = decodeBitmap(src, listener);
+ return new BitmapDrawable(src.getResources(), bitmap);
+ }
+
+ @LayoutlibDelegate
+ static Drawable decodeDrawable(@NonNull Source src) throws IOException {
+ return decodeDrawable(src, null);
+ }
+}
diff --git a/android/graphics/Paint.java b/android/graphics/Paint.java
index 317144a..5a80ee2 100644
--- a/android/graphics/Paint.java
+++ b/android/graphics/Paint.java
@@ -2742,7 +2742,7 @@
* @param offset index of caret position
* @return width measurement between start and offset
*/
- public float getRunAdvance(@NonNull CharSequence text, int start, int end, int contextStart,
+ public float getRunAdvance(CharSequence text, int start, int end, int contextStart,
int contextEnd, boolean isRtl, int offset) {
if (text == null) {
throw new IllegalArgumentException("text cannot be null");
diff --git a/android/graphics/PostProcess.java b/android/graphics/PostProcessor.java
similarity index 78%
rename from android/graphics/PostProcess.java
rename to android/graphics/PostProcessor.java
index c5a31e8..b1712e9 100644
--- a/android/graphics/PostProcess.java
+++ b/android/graphics/PostProcessor.java
@@ -20,38 +20,38 @@
import android.annotation.NonNull;
import android.graphics.drawable.Drawable;
-
/**
* Helper interface for adding custom processing to an image.
*
- * The image being processed may be a {@link Drawable}, {@link Bitmap} or frame
+ * <p>The image being processed may be a {@link Drawable}, {@link Bitmap} or frame
* of an animated image produced by {@link ImageDecoder}. This is called before
- * the requested object is returned.
+ * the requested object is returned.</p>
*
- * This custom processing also applies to image types that are otherwise
- * immutable, such as {@link Bitmap.Config#HARDWARE}.
+ * <p>This custom processing also applies to image types that are otherwise
+ * immutable, such as {@link Bitmap.Config#HARDWARE}.</p>
*
- * On an animated image, the callback will only be called once, but the drawing
+ * <p>On an animated image, the callback will only be called once, but the drawing
* commands will be applied to each frame, as if the {@code Canvas} had been
- * returned by {@link Picture#beginRecording}.
+ * returned by {@link Picture#beginRecording}.<p>
*
- * Supplied to ImageDecoder via {@link ImageDecoder#setPostProcess}.
- * @hide
+ * <p>Supplied to ImageDecoder via {@link ImageDecoder#setPostProcessor}.</p>
*/
-public interface PostProcess {
+public interface PostProcessor {
/**
* Do any processing after (for example) decoding.
*
- * Drawing to the {@link Canvas} will behave as if the initial processing
+ * <p>Drawing to the {@link Canvas} will behave as if the initial processing
* (e.g. decoding) already exists in the Canvas. An implementation can draw
* effects on top of this, or it can even draw behind it using
* {@link PorterDuff.Mode#DST_OVER}. A common effect is to add transparency
* to the corners to achieve rounded corners. That can be done with the
- * following code:
+ * following code:</p>
*
* <code>
* Path path = new Path();
* path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
+ * int width = canvas.getWidth();
+ * int height = canvas.getHeight();
* path.addRoundRect(0, 0, width, height, 20, 20, Path.Direction.CW);
* Paint paint = new Paint();
* paint.setAntiAlias(true);
@@ -63,10 +63,6 @@
*
*
* @param canvas The {@link Canvas} to draw to.
- * @param width Width of {@code canvas}. Anything drawn outside of this
- * will be ignored.
- * @param height Height of {@code canvas}. Anything drawn outside of this
- * will be ignored.
* @return Opacity of the result after drawing.
* {@link PixelFormat#UNKNOWN} means that the implementation did not
* change whether the image has alpha. Return this unless you added
@@ -87,5 +83,5 @@
* {@link java.lang.IllegalArgumentException}.
*/
@PixelFormat.Opacity
- public int postProcess(@NonNull Canvas canvas, int width, int height);
+ public int onPostProcess(@NonNull Canvas canvas);
}
diff --git a/android/graphics/Typeface.java b/android/graphics/Typeface.java
index 3d65bd2..ef41507 100644
--- a/android/graphics/Typeface.java
+++ b/android/graphics/Typeface.java
@@ -429,7 +429,7 @@
}
/**
- * Sets an index of the font collection.
+ * Sets an index of the font collection. See {@link android.R.attr#ttcIndex}.
*
* Can not be used for Typeface source. build() method will return null for invalid index.
* @param ttcIndex An index of the font collection. If the font source is not font
@@ -1025,6 +1025,10 @@
xmlFamily.getName(), fallback, languageTags, variant, cache, fontDir);
if (family != null) {
fallbackMap.valueAt(i).add(family);
+ } else if (defaultFamily != null) {
+ fallbackMap.valueAt(i).add(defaultFamily);
+ } else {
+ // There is no valid for for default fallback. Ignore.
}
}
}
diff --git a/android/graphics/drawable/AnimatedImageDrawable.java b/android/graphics/drawable/AnimatedImageDrawable.java
new file mode 100644
index 0000000..3034a10
--- /dev/null
+++ b/android/graphics/drawable/AnimatedImageDrawable.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2018 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.graphics.drawable;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.AssetFileDescriptor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.ImageDecoder;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.util.DisplayMetrics;
+
+import libcore.io.IoUtils;
+import libcore.util.NativeAllocationRegistry;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.Runnable;
+
+/**
+ * @hide
+ */
+public class AnimatedImageDrawable extends Drawable implements Animatable {
+ private final long mNativePtr;
+ private final InputStream mInputStream;
+ private final AssetFileDescriptor mAssetFd;
+
+ private final int mIntrinsicWidth;
+ private final int mIntrinsicHeight;
+
+ private Runnable mRunnable = new Runnable() {
+ @Override
+ public void run() {
+ invalidateSelf();
+ }
+ };
+
+ /**
+ * @hide
+ * This should only be called by ImageDecoder.
+ *
+ * decoder is only non-null if it has a PostProcess
+ */
+ public AnimatedImageDrawable(long nativeImageDecoder,
+ @Nullable ImageDecoder decoder, int width, int height,
+ int srcDensity, int dstDensity, Rect cropRect,
+ InputStream inputStream, AssetFileDescriptor afd)
+ throws IOException {
+ width = Bitmap.scaleFromDensity(width, srcDensity, dstDensity);
+ height = Bitmap.scaleFromDensity(height, srcDensity, dstDensity);
+
+ if (cropRect == null) {
+ mIntrinsicWidth = width;
+ mIntrinsicHeight = height;
+ } else {
+ cropRect.set(Bitmap.scaleFromDensity(cropRect.left, srcDensity, dstDensity),
+ Bitmap.scaleFromDensity(cropRect.top, srcDensity, dstDensity),
+ Bitmap.scaleFromDensity(cropRect.right, srcDensity, dstDensity),
+ Bitmap.scaleFromDensity(cropRect.bottom, srcDensity, dstDensity));
+ mIntrinsicWidth = cropRect.width();
+ mIntrinsicHeight = cropRect.height();
+ }
+
+ mNativePtr = nCreate(nativeImageDecoder, decoder, width, height, cropRect);
+ mInputStream = inputStream;
+ mAssetFd = afd;
+
+ // FIXME: Use the right size for the native allocation.
+ long nativeSize = 200;
+ NativeAllocationRegistry registry = new NativeAllocationRegistry(
+ AnimatedImageDrawable.class.getClassLoader(), nGetNativeFinalizer(), nativeSize);
+ registry.registerNativeAllocation(this, mNativePtr);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ // FIXME: It's a shame that we have *both* a native finalizer and a Java
+ // one. The native one is necessary to report how much memory is being
+ // used natively, and this one is necessary to close the input. An
+ // alternative might be to read the entire stream ahead of time, so we
+ // can eliminate the Java finalizer.
+ try {
+ IoUtils.closeQuietly(mInputStream);
+ IoUtils.closeQuietly(mAssetFd);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mIntrinsicWidth;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mIntrinsicHeight;
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ long nextUpdate = nDraw(mNativePtr, canvas.getNativeCanvasWrapper());
+ // a value <= 0 indicates that the drawable is stopped or that renderThread
+ // will manage the animation
+ if (nextUpdate > 0) {
+ scheduleSelf(mRunnable, nextUpdate);
+ }
+ }
+
+ @Override
+ public void setAlpha(@IntRange(from=0,to=255) int alpha) {
+ if (alpha < 0 || alpha > 255) {
+ throw new IllegalArgumentException("Alpha must be between 0 and"
+ + " 255! provided " + alpha);
+ }
+ nSetAlpha(mNativePtr, alpha);
+ invalidateSelf();
+ }
+
+ @Override
+ public int getAlpha() {
+ return nGetAlpha(mNativePtr);
+ }
+
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {
+ long nativeFilter = colorFilter == null ? 0 : colorFilter.getNativeInstance();
+ nSetColorFilter(mNativePtr, nativeFilter);
+ invalidateSelf();
+ }
+
+ @Override
+ public @PixelFormat.Opacity int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ // TODO: Add a Constant State?
+ // @Override
+ // public @Nullable ConstantState getConstantState() {}
+
+
+ // Animatable overrides
+ @Override
+ public boolean isRunning() {
+ return nIsRunning(mNativePtr);
+ }
+
+ @Override
+ public void start() {
+ if (nStart(mNativePtr)) {
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void stop() {
+ nStop(mNativePtr);
+ }
+
+ private static native long nCreate(long nativeImageDecoder,
+ @Nullable ImageDecoder decoder, int width, int height, Rect cropRect)
+ throws IOException;
+ private static native long nGetNativeFinalizer();
+ private static native long nDraw(long nativePtr, long canvasNativePtr);
+ private static native void nSetAlpha(long nativePtr, int alpha);
+ private static native int nGetAlpha(long nativePtr);
+ private static native void nSetColorFilter(long nativePtr, long nativeFilter);
+ private static native boolean nIsRunning(long nativePtr);
+ private static native boolean nStart(long nativePtr);
+ private static native void nStop(long nativePtr);
+ private static native long nNativeByteSize(long nativePtr);
+}
diff --git a/android/graphics/drawable/BitmapDrawable.java b/android/graphics/drawable/BitmapDrawable.java
index e3740e3..7ad062a 100644
--- a/android/graphics/drawable/BitmapDrawable.java
+++ b/android/graphics/drawable/BitmapDrawable.java
@@ -163,7 +163,7 @@
/**
* Create a drawable by opening a given file path and decoding the bitmap.
*/
- @SuppressWarnings("unused")
+ @SuppressWarnings({ "unused", "ChainingConstructorIgnoresParameter" })
public BitmapDrawable(Resources res, String filepath) {
this(new BitmapState(BitmapFactory.decodeFile(filepath)), null);
mBitmapState.mTargetDensity = mTargetDensity;
@@ -188,7 +188,7 @@
/**
* Create a drawable by decoding a bitmap from the given input stream.
*/
- @SuppressWarnings("unused")
+ @SuppressWarnings({ "unused", "ChainingConstructorIgnoresParameter" })
public BitmapDrawable(Resources res, java.io.InputStream is) {
this(new BitmapState(BitmapFactory.decodeStream(is)), null);
mBitmapState.mTargetDensity = mTargetDensity;
diff --git a/android/graphics/drawable/Icon.java b/android/graphics/drawable/Icon.java
index c329918..749b759 100644
--- a/android/graphics/drawable/Icon.java
+++ b/android/graphics/drawable/Icon.java
@@ -819,8 +819,10 @@
if (bitmapWidth > maxWidth || bitmapHeight > maxHeight) {
float scale = Math.min((float) maxWidth / bitmapWidth,
(float) maxHeight / bitmapHeight);
- bitmap = Bitmap.createScaledBitmap(bitmap, (int) (scale * bitmapWidth),
- (int) (scale * bitmapHeight), true /* filter */);
+ bitmap = Bitmap.createScaledBitmap(bitmap,
+ Math.max(1, (int) (scale * bitmapWidth)),
+ Math.max(1, (int) (scale * bitmapHeight)),
+ true /* filter */);
}
return bitmap;
}
diff --git a/android/graphics/drawable/RippleBackground.java b/android/graphics/drawable/RippleBackground.java
index dea194e..41d3698 100644
--- a/android/graphics/drawable/RippleBackground.java
+++ b/android/graphics/drawable/RippleBackground.java
@@ -16,17 +16,12 @@
package android.graphics.drawable;
-import android.animation.Animator;
-import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.graphics.Canvas;
-import android.graphics.CanvasProperty;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.FloatProperty;
-import android.view.DisplayListCanvas;
-import android.view.RenderNodeAnimator;
import android.view.animation.LinearInterpolator;
/**
@@ -68,30 +63,32 @@
}
}
- public void setState(boolean focused, boolean hovered, boolean animateChanged) {
+ public void setState(boolean focused, boolean hovered, boolean pressed) {
+ if (!mFocused) {
+ focused = focused && !pressed;
+ }
+ if (!mHovered) {
+ hovered = hovered && !pressed;
+ }
if (mHovered != hovered || mFocused != focused) {
mHovered = hovered;
mFocused = focused;
- onStateChanged(animateChanged);
+ onStateChanged();
}
}
- private void onStateChanged(boolean animateChanged) {
+ private void onStateChanged() {
float newOpacity = 0.0f;
- if (mHovered) newOpacity += 1.0f;
- if (mFocused) newOpacity += 1.0f;
+ if (mHovered) newOpacity += .25f;
+ if (mFocused) newOpacity += .75f;
if (mAnimator != null) {
mAnimator.cancel();
mAnimator = null;
}
- if (animateChanged) {
- mAnimator = ObjectAnimator.ofFloat(this, OPACITY, newOpacity);
- mAnimator.setDuration(OPACITY_DURATION);
- mAnimator.setInterpolator(LINEAR_INTERPOLATOR);
- mAnimator.start();
- } else {
- mOpacity = newOpacity;
- }
+ mAnimator = ObjectAnimator.ofFloat(this, OPACITY, newOpacity);
+ mAnimator.setDuration(OPACITY_DURATION);
+ mAnimator.setInterpolator(LINEAR_INTERPOLATOR);
+ mAnimator.start();
}
public void jumpToFinal() {
diff --git a/android/graphics/drawable/RippleDrawable.java b/android/graphics/drawable/RippleDrawable.java
index 734cff5..0da61c2 100644
--- a/android/graphics/drawable/RippleDrawable.java
+++ b/android/graphics/drawable/RippleDrawable.java
@@ -264,8 +264,8 @@
}
setRippleActive(enabled && pressed);
+ setBackgroundActive(hovered, focused, pressed);
- setBackgroundActive(hovered, focused);
return changed;
}
@@ -280,13 +280,13 @@
}
}
- private void setBackgroundActive(boolean hovered, boolean focused) {
+ private void setBackgroundActive(boolean hovered, boolean focused, boolean pressed) {
if (mBackground == null && (hovered || focused)) {
mBackground = new RippleBackground(this, mHotspotBounds, isBounded());
mBackground.setup(mState.mMaxRadius, mDensity);
}
if (mBackground != null) {
- mBackground.setState(focused, hovered, true);
+ mBackground.setState(focused, hovered, pressed);
}
}
@@ -878,23 +878,22 @@
// Grab the color for the current state and cut the alpha channel in
// half so that the ripple and background together yield full alpha.
- final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
- final int halfAlpha = (Color.alpha(color) / 2) << 24;
+ int color = mState.mColor.getColorForState(getState(), Color.BLACK);
+ if (Color.alpha(color) > 128) {
+ color = (color & 0x00FFFFFF) | 0x80000000;
+ }
final Paint p = mRipplePaint;
if (mMaskColorFilter != null) {
// The ripple timing depends on the paint's alpha value, so we need
// to push just the alpha channel into the paint and let the filter
// handle the full-alpha color.
- final int fullAlphaColor = color | (0xFF << 24);
- mMaskColorFilter.setColor(fullAlphaColor);
-
- p.setColor(halfAlpha);
+ mMaskColorFilter.setColor(color | 0xFF000000);
+ p.setColor(color & 0xFF000000);
p.setColorFilter(mMaskColorFilter);
p.setShader(mMaskShader);
} else {
- final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha;
- p.setColor(halfAlphaColor);
+ p.setColor(color);
p.setColorFilter(null);
p.setShader(null);
}
diff --git a/android/graphics/drawable/RippleForeground.java b/android/graphics/drawable/RippleForeground.java
index ecbf578..4129868 100644
--- a/android/graphics/drawable/RippleForeground.java
+++ b/android/graphics/drawable/RippleForeground.java
@@ -289,6 +289,7 @@
opacity.setInterpolator(LINEAR_INTERPOLATOR);
opacity.addListener(mAnimationListener);
opacity.setStartDelay(computeFadeOutDelay());
+ opacity.setStartValue(mOwner.getRipplePaint().getAlpha());
mPendingHwAnimators.add(opacity);
invalidateSelf();
}
diff --git a/android/hardware/HardwareBuffer.java b/android/hardware/HardwareBuffer.java
index 7866b52..9aa3f40 100644
--- a/android/hardware/HardwareBuffer.java
+++ b/android/hardware/HardwareBuffer.java
@@ -25,11 +25,11 @@
import dalvik.annotation.optimization.FastNative;
import dalvik.system.CloseGuard;
+import libcore.util.NativeAllocationRegistry;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import libcore.util.NativeAllocationRegistry;
-
/**
* HardwareBuffer wraps a native <code>AHardwareBuffer</code> object, which is a low-level object
* representing a memory buffer accessible by various hardware units. HardwareBuffer allows sharing
@@ -42,18 +42,25 @@
public final class HardwareBuffer implements Parcelable, AutoCloseable {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = { "RGB", "BLOB" }, value = {
+ @IntDef(prefix = { "RGB", "BLOB", "D_", "DS_", "S_" }, value = {
RGBA_8888,
RGBA_FP16,
RGBA_1010102,
RGBX_8888,
RGB_888,
RGB_565,
- BLOB
+ BLOB,
+ D_16,
+ D_24,
+ DS_24UI8,
+ D_FP32,
+ DS_FP32UI8,
+ S_UI8,
})
public @interface Format {
}
+ @Format
/** Format: 8 bits each red, green, blue, alpha */
public static final int RGBA_8888 = 1;
/** Format: 8 bits each red, green, blue, alpha, alpha is always 0xFF */
@@ -68,6 +75,18 @@
public static final int RGBA_1010102 = 0x2b;
/** Format: opaque format used for raw data transfer; must have a height of 1 */
public static final int BLOB = 0x21;
+ /** Format: 16 bits depth */
+ public static final int D_16 = 0x30;
+ /** Format: 24 bits depth */
+ public static final int D_24 = 0x31;
+ /** Format: 24 bits depth, 8 bits stencil */
+ public static final int DS_24UI8 = 0x32;
+ /** Format: 32 bits depth */
+ public static final int D_FP32 = 0x33;
+ /** Format: 32 bits depth, 8 bits stencil */
+ public static final int DS_FP32UI8 = 0x34;
+ /** Format: 8 bits stencil */
+ public static final int S_UI8 = 0x35;
// Note: do not rename, this field is used by native code
private long mNativeObject;
@@ -82,9 +101,11 @@
@LongDef(flag = true, value = {USAGE_CPU_READ_RARELY, USAGE_CPU_READ_OFTEN,
USAGE_CPU_WRITE_RARELY, USAGE_CPU_WRITE_OFTEN, USAGE_GPU_SAMPLED_IMAGE,
USAGE_GPU_COLOR_OUTPUT, USAGE_PROTECTED_CONTENT, USAGE_VIDEO_ENCODE,
- USAGE_GPU_DATA_BUFFER, USAGE_SENSOR_DIRECT_DATA})
+ USAGE_GPU_DATA_BUFFER, USAGE_SENSOR_DIRECT_DATA, USAGE_GPU_CUBE_MAP,
+ USAGE_GPU_MIPMAP_COMPLETE})
public @interface Usage {};
+ @Usage
/** Usage: The buffer will sometimes be read by the CPU */
public static final long USAGE_CPU_READ_RARELY = 2;
/** Usage: The buffer will often be read by the CPU */
@@ -107,6 +128,10 @@
public static final long USAGE_SENSOR_DIRECT_DATA = 1 << 23;
/** Usage: The buffer will be used as a shader storage or uniform buffer object */
public static final long USAGE_GPU_DATA_BUFFER = 1 << 24;
+ /** Usage: The buffer will be used as a cube map texture */
+ public static final long USAGE_GPU_CUBE_MAP = 1 << 25;
+ /** Usage: The buffer contains a complete mipmap hierarchy */
+ public static final long USAGE_GPU_MIPMAP_COMPLETE = 1 << 26;
// The approximate size of a native AHardwareBuffer object.
private static final long NATIVE_HARDWARE_BUFFER_SIZE = 232;
@@ -118,15 +143,9 @@
*
* @param width The width in pixels of the buffer
* @param height The height in pixels of the buffer
- * @param format The format of each pixel, one of {@link #RGBA_8888}, {@link #RGBA_FP16},
- * {@link #RGBX_8888}, {@link #RGB_565}, {@link #RGB_888}, {@link #RGBA_1010102}, {@link #BLOB}
+ * @param format The @Format of each pixel
* @param layers The number of layers in the buffer
- * @param usage Flags describing how the buffer will be used, one of
- * {@link #USAGE_CPU_READ_RARELY}, {@link #USAGE_CPU_READ_OFTEN},
- * {@link #USAGE_CPU_WRITE_RARELY}, {@link #USAGE_CPU_WRITE_OFTEN},
- * {@link #USAGE_GPU_SAMPLED_IMAGE}, {@link #USAGE_GPU_COLOR_OUTPUT},
- * {@link #USAGE_GPU_DATA_BUFFER}, {@link #USAGE_PROTECTED_CONTENT},
- * {@link #USAGE_SENSOR_DIRECT_DATA}, {@link #USAGE_VIDEO_ENCODE}
+ * @param usage The @Usage flags describing how the buffer will be used
* @return A <code>HardwareBuffer</code> instance if successful, or throws an
* IllegalArgumentException if the dimensions passed are invalid (either zero, negative, or
* too large to allocate), if the format is not supported, if the requested number of layers
@@ -154,7 +173,7 @@
if (nativeObject == 0) {
throw new IllegalArgumentException("Unable to create a HardwareBuffer, either the " +
"dimensions passed were too large, too many image layers were requested, " +
- "or an invalid set of usage flags was passed");
+ "or an invalid set of usage flags or invalid format was passed");
}
return new HardwareBuffer(nativeObject);
}
@@ -206,8 +225,7 @@
}
/**
- * Returns the format of this buffer, one of {@link #RGBA_8888}, {@link #RGBA_FP16},
- * {@link #RGBX_8888}, {@link #RGB_565}, {@link #RGB_888}, {@link #RGBA_1010102}, {@link #BLOB}.
+ * Returns the @Format of this buffer.
*/
@Format
public int getFormat() {
@@ -338,6 +356,12 @@
case RGB_565:
case RGB_888:
case BLOB:
+ case D_16:
+ case D_24:
+ case DS_24UI8:
+ case D_FP32:
+ case DS_FP32UI8:
+ case S_UI8:
return true;
}
return false;
diff --git a/android/hardware/camera2/CameraCharacteristics.java b/android/hardware/camera2/CameraCharacteristics.java
index 57ab18e..96d043c 100644
--- a/android/hardware/camera2/CameraCharacteristics.java
+++ b/android/hardware/camera2/CameraCharacteristics.java
@@ -22,9 +22,11 @@
import android.hardware.camera2.impl.PublicKey;
import android.hardware.camera2.impl.SyntheticKey;
import android.hardware.camera2.params.SessionConfiguration;
+import android.hardware.camera2.utils.ArrayUtils;
import android.hardware.camera2.utils.TypeReference;
import android.util.Rational;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -171,6 +173,7 @@
private List<CameraCharacteristics.Key<?>> mKeys;
private List<CaptureRequest.Key<?>> mAvailableRequestKeys;
private List<CaptureRequest.Key<?>> mAvailableSessionKeys;
+ private List<CaptureRequest.Key<?>> mAvailablePhysicalRequestKeys;
private List<CaptureResult.Key<?>> mAvailableResultKeys;
/**
@@ -314,6 +317,45 @@
}
/**
+ * <p>Returns a subset of {@link #getAvailableCaptureRequestKeys} keys that can
+ * be overriden for physical devices backing a logical multi-camera.</p>
+ *
+ * <p>This is a subset of android.request.availableRequestKeys which contains a list
+ * of keys that can be overriden using {@link CaptureRequest.Builder#setPhysicalCameraKey }.
+ * The respective value of such request key can be obtained by calling
+ * {@link CaptureRequest.Builder#getPhysicalCameraKey }. Capture requests that contain
+ * individual physical device requests must be built via
+ * {@link android.hardware.camera2.CameraDevice#createCaptureRequest(int, Set)}.
+ * Such extended capture requests can be passed only to
+ * {@link CameraCaptureSession#capture } or {@link CameraCaptureSession#captureBurst } and
+ * not to {@link CameraCaptureSession#setRepeatingRequest } or
+ * {@link CameraCaptureSession#setRepeatingBurst }.</p>
+ *
+ * <p>The list returned is not modifiable, so any attempts to modify it will throw
+ * a {@code UnsupportedOperationException}.</p>
+ *
+ * <p>Each key is only listed once in the list. The order of the keys is undefined.</p>
+ *
+ * @return List of keys that can be overriden in individual physical device requests.
+ * In case the camera device doesn't support such keys the list can be null.
+ */
+ @SuppressWarnings({"unchecked"})
+ public List<CaptureRequest.Key<?>> getAvailablePhysicalCameraRequestKeys() {
+ if (mAvailableSessionKeys == null) {
+ Object crKey = CaptureRequest.Key.class;
+ Class<CaptureRequest.Key<?>> crKeyTyped = (Class<CaptureRequest.Key<?>>)crKey;
+
+ int[] filterTags = get(REQUEST_AVAILABLE_PHYSICAL_CAMERA_REQUEST_KEYS);
+ if (filterTags == null) {
+ return null;
+ }
+ mAvailablePhysicalRequestKeys =
+ getAvailableKeyList(CaptureRequest.class, crKeyTyped, filterTags);
+ }
+ return mAvailablePhysicalRequestKeys;
+ }
+
+ /**
* Returns the list of keys supported by this {@link CameraDevice} for querying
* with a {@link CaptureRequest}.
*
@@ -407,6 +449,47 @@
return Collections.unmodifiableList(staticKeyList);
}
+ /**
+ * Returns the list of physical camera ids that this logical {@link CameraDevice} is
+ * made up of.
+ *
+ * <p>A camera device is a logical camera if it has
+ * REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA capability. If the camera device
+ * doesn't have the capability, the return value will be an empty list. </p>
+ *
+ * <p>The list returned is not modifiable, so any attempts to modify it will throw
+ * a {@code UnsupportedOperationException}.</p>
+ *
+ * <p>Each physical camera id is only listed once in the list. The order of the keys
+ * is undefined.</p>
+ *
+ * @return List of physical camera ids for this logical camera device.
+ */
+ @NonNull
+ public List<String> getPhysicalCameraIds() {
+ int[] availableCapabilities = get(REQUEST_AVAILABLE_CAPABILITIES);
+ if (availableCapabilities == null) {
+ throw new AssertionError("android.request.availableCapabilities must be non-null "
+ + "in the characteristics");
+ }
+
+ if (!ArrayUtils.contains(availableCapabilities,
+ REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)) {
+ return Collections.emptyList();
+ }
+ byte[] physicalCamIds = get(LOGICAL_MULTI_CAMERA_PHYSICAL_IDS);
+
+ String physicalCamIdString = null;
+ try {
+ physicalCamIdString = new String(physicalCamIds, "UTF-8");
+ } catch (java.io.UnsupportedEncodingException e) {
+ throw new AssertionError("android.logicalCam.physicalIds must be UTF-8 string");
+ }
+ String[] physicalCameraIdList = physicalCamIdString.split("\0");
+
+ return Collections.unmodifiableList(Arrays.asList(physicalCameraIdList));
+ }
+
/*@O~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~
* The key entries below this point are generated from metadata
* definitions in /system/media/camera/docs. Do not modify by hand or
@@ -1149,36 +1232,33 @@
/**
* <p>Position of the camera optical center.</p>
* <p>The position of the camera device's lens optical center,
- * as a three-dimensional vector <code>(x,y,z)</code>, relative to the
- * optical center of the largest camera device facing in the
- * same direction as this camera, in the {@link android.hardware.SensorEvent Android sensor coordinate
- * axes}. Note that only the axis definitions are shared with
- * the sensor coordinate system, but not the origin.</p>
- * <p>If this device is the largest or only camera device with a
- * given facing, then this position will be <code>(0, 0, 0)</code>; a
- * camera device with a lens optical center located 3 cm from
- * the main sensor along the +X axis (to the right from the
- * user's perspective) will report <code>(0.03, 0, 0)</code>.</p>
- * <p>To transform a pixel coordinates between two cameras
- * facing the same direction, first the source camera
- * {@link CameraCharacteristics#LENS_RADIAL_DISTORTION android.lens.radialDistortion} must be corrected for. Then
- * the source camera {@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration} needs
- * to be applied, followed by the {@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation}
- * of the source camera, the translation of the source camera
- * relative to the destination camera, the
- * {@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation} of the destination camera, and
- * finally the inverse of {@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration}
- * of the destination camera. This obtains a
- * radial-distortion-free coordinate in the destination
- * camera pixel coordinates.</p>
- * <p>To compare this against a real image from the destination
- * camera, the destination camera image then needs to be
- * corrected for radial distortion before comparison or
- * sampling.</p>
+ * as a three-dimensional vector <code>(x,y,z)</code>.</p>
+ * <p>Prior to Android P, or when {@link CameraCharacteristics#LENS_POSE_REFERENCE android.lens.poseReference} is PRIMARY_CAMERA, this position
+ * is relative to the optical center of the largest camera device facing in the same
+ * direction as this camera, in the {@link android.hardware.SensorEvent Android sensor
+ * coordinate axes}. Note that only the axis definitions are shared with the sensor
+ * coordinate system, but not the origin.</p>
+ * <p>If this device is the largest or only camera device with a given facing, then this
+ * position will be <code>(0, 0, 0)</code>; a camera device with a lens optical center located 3 cm
+ * from the main sensor along the +X axis (to the right from the user's perspective) will
+ * report <code>(0.03, 0, 0)</code>.</p>
+ * <p>To transform a pixel coordinates between two cameras facing the same direction, first
+ * the source camera {@link CameraCharacteristics#LENS_RADIAL_DISTORTION android.lens.radialDistortion} must be corrected for. Then the source
+ * camera {@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration} needs to be applied, followed by the
+ * {@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation} of the source camera, the translation of the source camera
+ * relative to the destination camera, the {@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation} of the destination
+ * camera, and finally the inverse of {@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration} of the destination
+ * camera. This obtains a radial-distortion-free coordinate in the destination camera pixel
+ * coordinates.</p>
+ * <p>To compare this against a real image from the destination camera, the destination camera
+ * image then needs to be corrected for radial distortion before comparison or sampling.</p>
+ * <p>When {@link CameraCharacteristics#LENS_POSE_REFERENCE android.lens.poseReference} is GYROSCOPE, then this position is relative to
+ * the center of the primary gyroscope on the device.</p>
* <p><b>Units</b>: Meters</p>
* <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
*
* @see CameraCharacteristics#LENS_INTRINSIC_CALIBRATION
+ * @see CameraCharacteristics#LENS_POSE_REFERENCE
* @see CameraCharacteristics#LENS_POSE_ROTATION
* @see CameraCharacteristics#LENS_RADIAL_DISTORTION
*/
@@ -1289,6 +1369,28 @@
new Key<float[]>("android.lens.radialDistortion", float[].class);
/**
+ * <p>The origin for {@link CameraCharacteristics#LENS_POSE_TRANSLATION android.lens.poseTranslation}.</p>
+ * <p>Different calibration methods and use cases can produce better or worse results
+ * depending on the selected coordinate origin.</p>
+ * <p>For devices designed to support the MOTION_TRACKING capability, the GYROSCOPE origin
+ * makes device calibration and later usage by applications combining camera and gyroscope
+ * information together simpler.</p>
+ * <p><b>Possible values:</b>
+ * <ul>
+ * <li>{@link #LENS_POSE_REFERENCE_PRIMARY_CAMERA PRIMARY_CAMERA}</li>
+ * <li>{@link #LENS_POSE_REFERENCE_GYROSCOPE GYROSCOPE}</li>
+ * </ul></p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ *
+ * @see CameraCharacteristics#LENS_POSE_TRANSLATION
+ * @see #LENS_POSE_REFERENCE_PRIMARY_CAMERA
+ * @see #LENS_POSE_REFERENCE_GYROSCOPE
+ */
+ @PublicKey
+ public static final Key<Integer> LENS_POSE_REFERENCE =
+ new Key<Integer>("android.lens.poseReference", int.class);
+
+ /**
* <p>List of noise reduction modes for {@link CaptureRequest#NOISE_REDUCTION_MODE android.noiseReduction.mode} that are supported
* by this camera device.</p>
* <p>Full-capability camera devices will always support OFF and FAST.</p>
@@ -1559,6 +1661,8 @@
* <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING YUV_REPROCESSING}</li>
* <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT DEPTH_OUTPUT}</li>
* <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO CONSTRAINED_HIGH_SPEED_VIDEO}</li>
+ * <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING MOTION_TRACKING}</li>
+ * <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA LOGICAL_MULTI_CAMERA}</li>
* </ul></p>
* <p>This key is available on all devices.</p>
*
@@ -1573,6 +1677,8 @@
* @see #REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING
* @see #REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT
* @see #REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO
+ * @see #REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING
+ * @see #REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
*/
@PublicKey
public static final Key<int[]> REQUEST_AVAILABLE_CAPABILITIES =
@@ -1642,8 +1748,9 @@
* lifetime. Typical examples include parameters that require a
* time-consuming hardware re-configuration or internal camera pipeline
* change. For performance reasons we advise clients to pass their initial
- * values as part of {@link SessionConfiguration#setSessionParameters }. Once
- * the camera capture session is enabled it is also recommended to avoid
+ * values as part of
+ * {@link SessionConfiguration#setSessionParameters }.
+ * Once the camera capture session is enabled it is also recommended to avoid
* changing them from their initial values set in
* {@link SessionConfiguration#setSessionParameters }.
* Control over session parameters can still be exerted in capture requests
@@ -1653,15 +1760,18 @@
* <li>The camera client starts by quering the session parameter key list via
* {@link android.hardware.camera2.CameraCharacteristics#getAvailableSessionKeys }.</li>
* <li>Before triggering the capture session create sequence, a capture request
- * must be built via {@link CameraDevice#createCaptureRequest } using an
- * appropriate template matching the particular use case.</li>
+ * must be built via
+ * {@link CameraDevice#createCaptureRequest }
+ * using an appropriate template matching the particular use case.</li>
* <li>The client should go over the list of session parameters and check
* whether some of the keys listed matches with the parameters that
* they intend to modify as part of the first capture request.</li>
* <li>If there is no such match, the capture request can be passed
- * unmodified to {@link SessionConfiguration#setSessionParameters }.</li>
+ * unmodified to
+ * {@link SessionConfiguration#setSessionParameters }.</li>
* <li>If matches do exist, the client should update the respective values
- * and pass the request to {@link SessionConfiguration#setSessionParameters }.</li>
+ * and pass the request to
+ * {@link SessionConfiguration#setSessionParameters }.</li>
* <li>After the capture session initialization completes the session parameter
* key list can continue to serve as reference when posting or updating
* further requests. As mentioned above further changes to session
@@ -1676,6 +1786,30 @@
new Key<int[]>("android.request.availableSessionKeys", int[].class);
/**
+ * <p>A subset of the available request keys that can be overriden for
+ * physical devices backing a logical multi-camera.</p>
+ * <p>This is a subset of android.request.availableRequestKeys which contains a list
+ * of keys that can be overriden using {@link CaptureRequest.Builder#setPhysicalCameraKey }.
+ * The respective value of such request key can be obtained by calling
+ * {@link CaptureRequest.Builder#getPhysicalCameraKey }. Capture requests that contain
+ * individual physical device requests must be built via
+ * {@link android.hardware.camera2.CameraDevice#createCaptureRequest(int, Set)}.
+ * Such extended capture requests can be passed only to
+ * {@link CameraCaptureSession#capture } or {@link CameraCaptureSession#captureBurst } and
+ * not to {@link CameraCaptureSession#setRepeatingRequest } or
+ * {@link CameraCaptureSession#setRepeatingBurst }.</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ * <p><b>Limited capability</b> -
+ * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+ * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+ *
+ * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+ * @hide
+ */
+ public static final Key<int[]> REQUEST_AVAILABLE_PHYSICAL_CAMERA_REQUEST_KEYS =
+ new Key<int[]>("android.request.availablePhysicalCameraRequestKeys", int[].class);
+
+ /**
* <p>The list of image formats that are supported by this
* camera device for output streams.</p>
* <p>All camera devices will support JPEG and YUV_420_888 formats.</p>
@@ -2845,6 +2979,21 @@
new Key<int[]>("android.statistics.info.availableLensShadingMapModes", int[].class);
/**
+ * <p>List of OIS data output modes for {@link CaptureRequest#STATISTICS_OIS_DATA_MODE android.statistics.oisDataMode} that
+ * are supported by this camera device.</p>
+ * <p>If no OIS data output is available for this camera device, this key will
+ * contain only OFF.</p>
+ * <p><b>Range of valid values:</b><br>
+ * Any value listed in {@link CaptureRequest#STATISTICS_OIS_DATA_MODE android.statistics.oisDataMode}</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ *
+ * @see CaptureRequest#STATISTICS_OIS_DATA_MODE
+ */
+ @PublicKey
+ public static final Key<int[]> STATISTICS_INFO_AVAILABLE_OIS_DATA_MODES =
+ new Key<int[]>("android.statistics.info.availableOisDataModes", int[].class);
+
+ /**
* <p>Maximum number of supported points in the
* tonemap curve that can be used for {@link CaptureRequest#TONEMAP_CURVE android.tonemap.curve}.</p>
* <p>If the actual number of points provided by the application (in {@link CaptureRequest#TONEMAP_CURVE android.tonemap.curve}*) is
@@ -2953,6 +3102,7 @@
* <li>{@link #INFO_SUPPORTED_HARDWARE_LEVEL_FULL FULL}</li>
* <li>{@link #INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY LEGACY}</li>
* <li>{@link #INFO_SUPPORTED_HARDWARE_LEVEL_3 3}</li>
+ * <li>{@link #INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL EXTERNAL}</li>
* </ul></p>
* <p>This key is available on all devices.</p>
*
@@ -2966,12 +3116,25 @@
* @see #INFO_SUPPORTED_HARDWARE_LEVEL_FULL
* @see #INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
* @see #INFO_SUPPORTED_HARDWARE_LEVEL_3
+ * @see #INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL
*/
@PublicKey
public static final Key<Integer> INFO_SUPPORTED_HARDWARE_LEVEL =
new Key<Integer>("android.info.supportedHardwareLevel", int.class);
/**
+ * <p>A short string for manufacturer version information about the camera device, such as
+ * ISP hardware, sensors, etc.</p>
+ * <p>This can be used in {@link android.media.ExifInterface#TAG_IMAGE_DESCRIPTION TAG_IMAGE_DESCRIPTION}
+ * in jpeg EXIF. This key may be absent if no version information is available on the
+ * device.</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ */
+ @PublicKey
+ public static final Key<String> INFO_VERSION =
+ new Key<String>("android.info.version", String.class);
+
+ /**
* <p>The maximum number of frames that can occur after a request
* (different than the previous) has been submitted, and before the
* result's state becomes synchronized.</p>
@@ -3130,6 +3293,54 @@
public static final Key<Boolean> DEPTH_DEPTH_IS_EXCLUSIVE =
new Key<Boolean>("android.depth.depthIsExclusive", boolean.class);
+ /**
+ * <p>String containing the ids of the underlying physical cameras.</p>
+ * <p>For a logical camera, this is concatenation of all underlying physical camera ids.
+ * The null terminator for physical camera id must be preserved so that the whole string
+ * can be tokenized using '\0' to generate list of physical camera ids.</p>
+ * <p>For example, if the physical camera ids of the logical camera are "2" and "3", the
+ * value of this tag will be ['2', '\0', '3', '\0'].</p>
+ * <p>The number of physical camera ids must be no less than 2.</p>
+ * <p><b>Units</b>: UTF-8 null-terminated string</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ * <p><b>Limited capability</b> -
+ * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+ * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+ *
+ * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+ * @hide
+ */
+ public static final Key<byte[]> LOGICAL_MULTI_CAMERA_PHYSICAL_IDS =
+ new Key<byte[]>("android.logicalMultiCamera.physicalIds", byte[].class);
+
+ /**
+ * <p>The accuracy of frame timestamp synchronization between physical cameras</p>
+ * <p>The accuracy of the frame timestamp synchronization determines the physical cameras'
+ * ability to start exposure at the same time. If the sensorSyncType is CALIBRATED,
+ * the physical camera sensors usually run in master-slave mode so that their shutter
+ * time is synchronized. For APPROXIMATE sensorSyncType, the camera sensors usually run in
+ * master-master mode, and there could be offset between their start of exposure.</p>
+ * <p>In both cases, all images generated for a particular capture request still carry the same
+ * timestamps, so that they can be used to look up the matching frame number and
+ * onCaptureStarted callback.</p>
+ * <p><b>Possible values:</b>
+ * <ul>
+ * <li>{@link #LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE_APPROXIMATE APPROXIMATE}</li>
+ * <li>{@link #LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE_CALIBRATED CALIBRATED}</li>
+ * </ul></p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ * <p><b>Limited capability</b> -
+ * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+ * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+ *
+ * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+ * @see #LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE_APPROXIMATE
+ * @see #LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE_CALIBRATED
+ */
+ @PublicKey
+ public static final Key<Integer> LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE =
+ new Key<Integer>("android.logicalMultiCamera.sensorSyncType", int.class);
+
/*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~
* End generated code
*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/
diff --git a/android/hardware/camera2/CameraDevice.java b/android/hardware/camera2/CameraDevice.java
index 87e503d..40ee834 100644
--- a/android/hardware/camera2/CameraDevice.java
+++ b/android/hardware/camera2/CameraDevice.java
@@ -31,6 +31,7 @@
import android.view.Surface;
import java.util.List;
+import java.util.Set;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -144,6 +145,37 @@
*/
public static final int TEMPLATE_MANUAL = 6;
+ /**
+ * A template for selecting camera parameters that match TEMPLATE_PREVIEW as closely as
+ * possible while improving the camera output for motion tracking use cases.
+ *
+ * <p>This template is best used by applications that are frequently switching between motion
+ * tracking use cases and regular still capture use cases, to minimize the IQ changes
+ * when swapping use cases.</p>
+ *
+ * <p>This template is guaranteed to be supported on camera devices that support the
+ * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING MOTION_TRACKING}
+ * capability.</p>
+ *
+ * @see #createCaptureRequest
+ */
+ public static final int TEMPLATE_MOTION_TRACKING_PREVIEW = 7;
+
+ /**
+ * A template for selecting camera parameters that maximize the quality of camera output for
+ * motion tracking use cases.
+ *
+ * <p>This template is best used by applications dedicated to motion tracking applications,
+ * which aren't concerned about fast switches between motion tracking and other use cases.</p>
+ *
+ * <p>This template is guaranteed to be supported on camera devices that support the
+ * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING MOTION_TRACKING}
+ * capability.</p>
+ *
+ * @see #createCaptureRequest
+ */
+ public static final int TEMPLATE_MOTION_TRACKING_BEST = 8;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"TEMPLATE_"}, value =
@@ -152,7 +184,9 @@
TEMPLATE_RECORD,
TEMPLATE_VIDEO_SNAPSHOT,
TEMPLATE_ZERO_SHUTTER_LAG,
- TEMPLATE_MANUAL })
+ TEMPLATE_MANUAL,
+ TEMPLATE_MOTION_TRACKING_PREVIEW,
+ TEMPLATE_MOTION_TRACKING_BEST})
public @interface RequestTemplate {};
/**
@@ -386,6 +420,27 @@
* </table><br>
* </p>
*
+ * <p>MOTION_TRACKING-capability ({@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES}
+ * includes
+ * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING MOTION_TRACKING})
+ * devices support at least the below stream combinations in addition to those for
+ * {@link CameraMetadata#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED LIMITED} devices. The
+ * {@code FULL FOV 640} entry means that the device will support a resolution that's 640 pixels
+ * wide, with the height set so that the resolution aspect ratio matches the MAXIMUM output
+ * aspect ratio, rounded down. So for a device with a 4:3 image sensor, this will be 640x480,
+ * and for a device with a 16:9 sensor, this will be 640x360, and so on. And the
+ * {@code MAX 30FPS} entry means the largest JPEG resolution on the device for which
+ * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputMinFrameDuration}
+ * returns a value less than or equal to 1/30s.
+ *
+ * <table>
+ * <tr><th colspan="7">MOTION_TRACKING-capability additional guaranteed configurations</th></tr>
+ * <tr><th colspan="2" id="rb">Target 1</th><th colspan="2" id="rb">Target 2</th><th colspan="2" id="rb">Target 3</th><th rowspan="2">Sample use case(s)</th> </tr>
+ * <tr><th>Type</th><th id="rb">Max size</th><th>Type</th><th id="rb">Max size</th><th>Type</th><th id="rb">Max size</th></tr>
+ * <tr> <td>{@code YUV}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV }</td><td id="rb">{@code FULL FOV 640}</td> <td>{@code JPEG}</td><td id="rb">{@code MAX 30FPS}</td> <td>Preview with a tracking YUV output and a as-large-as-possible JPEG for still captures.</td> </tr>
+ * </table><br>
+ * </p>
+ *
* <p>BURST-capability ({@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES} includes
* {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE BURST_CAPTURE}) devices
* support at least the below stream combinations in addition to those for
@@ -850,16 +905,51 @@
* @throws CameraAccessException if the camera device is no longer connected or has
* encountered a fatal error
* @throws IllegalStateException if the camera device has been closed
+ */
+ @NonNull
+ public abstract CaptureRequest.Builder createCaptureRequest(@RequestTemplate int templateType)
+ throws CameraAccessException;
+
+ /**
+ * <p>Create a {@link CaptureRequest.Builder} for new capture requests,
+ * initialized with template for a target use case. This methods allows
+ * clients to pass physical camera ids which can be used to customize the
+ * request for a specific physical camera. The settings are chosen
+ * to be the best options for the specific logical camera device. If
+ * additional physical camera ids are passed, then they will also use the
+ * same settings template. Requests containing individual physical camera
+ * settings can be passed only to {@link CameraCaptureSession#capture} or
+ * {@link CameraCaptureSession#captureBurst} and not to
+ * {@link CameraCaptureSession#setRepeatingRequest} or
+ * {@link CameraCaptureSession#setRepeatingBurst}</p>
+ *
+ * @param templateType An enumeration selecting the use case for this request. Not all template
+ * types are supported on every device. See the documentation for each template type for
+ * details.
+ * @param physicalCameraIdSet A set of physical camera ids that can be used to customize
+ * the request for a specific physical camera.
+ * @return a builder for a capture request, initialized with default
+ * settings for that template, and no output streams
+ *
+ * @throws IllegalArgumentException if the templateType is not supported by
+ * this device, or one of the physical id arguments matches with logical camera id.
+ * @throws CameraAccessException if the camera device is no longer connected or has
+ * encountered a fatal error
+ * @throws IllegalStateException if the camera device has been closed
*
* @see #TEMPLATE_PREVIEW
* @see #TEMPLATE_RECORD
* @see #TEMPLATE_STILL_CAPTURE
* @see #TEMPLATE_VIDEO_SNAPSHOT
* @see #TEMPLATE_MANUAL
+ * @see CaptureRequest.Builder#setKey
+ * @see CaptureRequest.Builder#getKey
*/
@NonNull
- public abstract CaptureRequest.Builder createCaptureRequest(@RequestTemplate int templateType)
- throws CameraAccessException;
+ public CaptureRequest.Builder createCaptureRequest(@RequestTemplate int templateType,
+ Set<String> physicalCameraIdSet) throws CameraAccessException {
+ throw new UnsupportedOperationException("Subclasses must override this method");
+ }
/**
* <p>Create a {@link CaptureRequest.Builder} for a new reprocess {@link CaptureRequest} from a
diff --git a/android/hardware/camera2/CameraManager.java b/android/hardware/camera2/CameraManager.java
index 90bf896..a2bc91e 100644
--- a/android/hardware/camera2/CameraManager.java
+++ b/android/hardware/camera2/CameraManager.java
@@ -996,7 +996,12 @@
return;
}
- Integer oldStatus = mDeviceStatus.put(id, status);
+ Integer oldStatus;
+ if (status == ICameraServiceListener.STATUS_NOT_PRESENT) {
+ oldStatus = mDeviceStatus.remove(id);
+ } else {
+ oldStatus = mDeviceStatus.put(id, status);
+ }
if (oldStatus != null && oldStatus == status) {
if (DEBUG) {
diff --git a/android/hardware/camera2/CameraMetadata.java b/android/hardware/camera2/CameraMetadata.java
index cb11d0f..e7c8961 100644
--- a/android/hardware/camera2/CameraMetadata.java
+++ b/android/hardware/camera2/CameraMetadata.java
@@ -336,6 +336,30 @@
public static final int LENS_FACING_EXTERNAL = 2;
//
+ // Enumeration values for CameraCharacteristics#LENS_POSE_REFERENCE
+ //
+
+ /**
+ * <p>The value of {@link CameraCharacteristics#LENS_POSE_TRANSLATION android.lens.poseTranslation} is relative to the optical center of
+ * the largest camera device facing the same direction as this camera.</p>
+ * <p>This default value for API levels before Android P.</p>
+ *
+ * @see CameraCharacteristics#LENS_POSE_TRANSLATION
+ * @see CameraCharacteristics#LENS_POSE_REFERENCE
+ */
+ public static final int LENS_POSE_REFERENCE_PRIMARY_CAMERA = 0;
+
+ /**
+ * <p>The value of {@link CameraCharacteristics#LENS_POSE_TRANSLATION android.lens.poseTranslation} is relative to the position of the
+ * primary gyroscope of this Android device.</p>
+ * <p>This is the value reported by all devices that support the MOTION_TRACKING capability.</p>
+ *
+ * @see CameraCharacteristics#LENS_POSE_TRANSLATION
+ * @see CameraCharacteristics#LENS_POSE_REFERENCE
+ */
+ public static final int LENS_POSE_REFERENCE_GYROSCOPE = 1;
+
+ //
// Enumeration values for CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES
//
@@ -665,6 +689,7 @@
* </ul>
* </li>
* <li>The {@link CameraCharacteristics#DEPTH_DEPTH_IS_EXCLUSIVE android.depth.depthIsExclusive} entry is listed by this device.</li>
+ * <li>As of Android P, the {@link CameraCharacteristics#LENS_POSE_REFERENCE android.lens.poseReference} entry is listed by this device.</li>
* <li>A LIMITED camera with only the DEPTH_OUTPUT capability does not have to support
* normal YUV_420_888, JPEG, and PRIV-format outputs. It only has to support the DEPTH16
* format.</li>
@@ -680,6 +705,7 @@
* @see CameraCharacteristics#DEPTH_DEPTH_IS_EXCLUSIVE
* @see CameraCharacteristics#LENS_FACING
* @see CameraCharacteristics#LENS_INTRINSIC_CALIBRATION
+ * @see CameraCharacteristics#LENS_POSE_REFERENCE
* @see CameraCharacteristics#LENS_POSE_ROTATION
* @see CameraCharacteristics#LENS_POSE_TRANSLATION
* @see CameraCharacteristics#LENS_RADIAL_DISTORTION
@@ -774,6 +800,98 @@
*/
public static final int REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO = 9;
+ /**
+ * <p>The device supports controls and metadata required for accurate motion tracking for
+ * use cases such as augmented reality, electronic image stabilization, and so on.</p>
+ * <p>This means this camera device has accurate optical calibration and timestamps relative
+ * to the inertial sensors.</p>
+ * <p>This capability requires the camera device to support the following:</p>
+ * <ul>
+ * <li>Capture request templates {@link android.hardware.camera2.CameraDevice#TEMPLATE_MOTION_TRACKING_PREVIEW } and {@link android.hardware.camera2.CameraDevice#TEMPLATE_MOTION_TRACKING_BEST } are defined.</li>
+ * <li>The stream configurations listed in {@link android.hardware.camera2.CameraDevice#createCaptureSession } for MOTION_TRACKING are
+ * supported, either at 30 or 60fps maximum frame rate.</li>
+ * <li>The following camera characteristics and capture result metadata are provided:<ul>
+ * <li>{@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration}</li>
+ * <li>{@link CameraCharacteristics#LENS_RADIAL_DISTORTION android.lens.radialDistortion}</li>
+ * <li>{@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation}</li>
+ * <li>{@link CameraCharacteristics#LENS_POSE_TRANSLATION android.lens.poseTranslation}</li>
+ * <li>{@link CameraCharacteristics#LENS_POSE_REFERENCE android.lens.poseReference} with value GYROSCOPE</li>
+ * </ul>
+ * </li>
+ * <li>The {@link CameraCharacteristics#SENSOR_INFO_TIMESTAMP_SOURCE android.sensor.info.timestampSource} field has value <code>REALTIME</code>. When compared to
+ * timestamps from the device's gyroscopes, the clock difference for events occuring at
+ * the same actual time instant will be less than 1 ms.</li>
+ * <li>The value of the {@link CaptureResult#SENSOR_ROLLING_SHUTTER_SKEW android.sensor.rollingShutterSkew} field is accurate to within 1 ms.</li>
+ * <li>The value of {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime} is guaranteed to be available in the
+ * capture result.</li>
+ * <li>The {@link CaptureRequest#CONTROL_CAPTURE_INTENT android.control.captureIntent} control supports MOTION_TRACKING to limit maximum
+ * exposure to 20 milliseconds.</li>
+ * <li>The stream configurations required for MOTION_TRACKING (listed at {@link android.hardware.camera2.CameraDevice#createCaptureSession }) can operate at least at
+ * 30fps; optionally, they can operate at 60fps, and '[60, 60]' is listed in
+ * {@link CameraCharacteristics#CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES android.control.aeAvailableTargetFpsRanges}.</li>
+ * </ul>
+ *
+ * @see CameraCharacteristics#CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES
+ * @see CaptureRequest#CONTROL_CAPTURE_INTENT
+ * @see CameraCharacteristics#LENS_INTRINSIC_CALIBRATION
+ * @see CameraCharacteristics#LENS_POSE_REFERENCE
+ * @see CameraCharacteristics#LENS_POSE_ROTATION
+ * @see CameraCharacteristics#LENS_POSE_TRANSLATION
+ * @see CameraCharacteristics#LENS_RADIAL_DISTORTION
+ * @see CaptureRequest#SENSOR_EXPOSURE_TIME
+ * @see CameraCharacteristics#SENSOR_INFO_TIMESTAMP_SOURCE
+ * @see CaptureResult#SENSOR_ROLLING_SHUTTER_SKEW
+ * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES
+ */
+ public static final int REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING = 10;
+
+ /**
+ * <p>The camera device is a logical camera backed by two or more physical cameras that are
+ * also exposed to the application.</p>
+ * <p>This capability requires the camera device to support the following:</p>
+ * <ul>
+ * <li>This camera device must list the following static metadata entries in {@link android.hardware.camera2.CameraCharacteristics }:<ul>
+ * <li>android.logicalMultiCamera.physicalIds</li>
+ * <li>{@link CameraCharacteristics#LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE android.logicalMultiCamera.sensorSyncType}</li>
+ * </ul>
+ * </li>
+ * <li>The underlying physical cameras' static metadata must list the following entries,
+ * so that the application can correlate pixels from the physical streams:<ul>
+ * <li>{@link CameraCharacteristics#LENS_POSE_REFERENCE android.lens.poseReference}</li>
+ * <li>{@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation}</li>
+ * <li>{@link CameraCharacteristics#LENS_POSE_TRANSLATION android.lens.poseTranslation}</li>
+ * <li>{@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration}</li>
+ * <li>{@link CameraCharacteristics#LENS_RADIAL_DISTORTION android.lens.radialDistortion}</li>
+ * </ul>
+ * </li>
+ * <li>The logical camera device must be LIMITED or higher device.</li>
+ * </ul>
+ * <p>Both the logical camera device and its underlying physical devices support the
+ * mandatory stream combinations required for their device levels.</p>
+ * <p>Additionally, for each guaranteed stream combination, the logical camera supports:</p>
+ * <ul>
+ * <li>Replacing one logical {@link android.graphics.ImageFormat#YUV_420_888 YUV_420_888}
+ * or raw stream with two physical streams of the same size and format, each from a
+ * separate physical camera, given that the size and format are supported by both
+ * physical cameras.</li>
+ * <li>Adding two raw streams, each from one physical camera, if the logical camera doesn't
+ * advertise RAW capability, but the underlying physical cameras do. This is usually
+ * the case when the physical cameras have different sensor sizes.</li>
+ * </ul>
+ * <p>Using physical streams in place of a logical stream of the same size and format will
+ * not slow down the frame rate of the capture, as long as the minimum frame duration
+ * of the physical and logical streams are the same.</p>
+ *
+ * @see CameraCharacteristics#LENS_INTRINSIC_CALIBRATION
+ * @see CameraCharacteristics#LENS_POSE_REFERENCE
+ * @see CameraCharacteristics#LENS_POSE_ROTATION
+ * @see CameraCharacteristics#LENS_POSE_TRANSLATION
+ * @see CameraCharacteristics#LENS_RADIAL_DISTORTION
+ * @see CameraCharacteristics#LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE
+ * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES
+ */
+ public static final int REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA = 11;
+
//
// Enumeration values for CameraCharacteristics#SCALER_CROPPING_TYPE
//
@@ -1063,6 +1181,38 @@
*/
public static final int INFO_SUPPORTED_HARDWARE_LEVEL_3 = 3;
+ /**
+ * <p>This camera device is backed by an external camera connected to this Android device.</p>
+ * <p>The device has capability identical to a LIMITED level device, with the following
+ * exceptions:</p>
+ * <ul>
+ * <li>The device may not report lens/sensor related information such as<ul>
+ * <li>{@link CaptureRequest#LENS_FOCAL_LENGTH android.lens.focalLength}</li>
+ * <li>{@link CameraCharacteristics#LENS_INFO_HYPERFOCAL_DISTANCE android.lens.info.hyperfocalDistance}</li>
+ * <li>{@link CameraCharacteristics#SENSOR_INFO_PHYSICAL_SIZE android.sensor.info.physicalSize}</li>
+ * <li>{@link CameraCharacteristics#SENSOR_INFO_WHITE_LEVEL android.sensor.info.whiteLevel}</li>
+ * <li>{@link CameraCharacteristics#SENSOR_BLACK_LEVEL_PATTERN android.sensor.blackLevelPattern}</li>
+ * <li>{@link CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT android.sensor.info.colorFilterArrangement}</li>
+ * <li>{@link CaptureResult#SENSOR_ROLLING_SHUTTER_SKEW android.sensor.rollingShutterSkew}</li>
+ * </ul>
+ * </li>
+ * <li>The device will report 0 for {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}</li>
+ * <li>The device has less guarantee on stable framerate, as the framerate partly depends
+ * on the external camera being used.</li>
+ * </ul>
+ *
+ * @see CaptureRequest#LENS_FOCAL_LENGTH
+ * @see CameraCharacteristics#LENS_INFO_HYPERFOCAL_DISTANCE
+ * @see CameraCharacteristics#SENSOR_BLACK_LEVEL_PATTERN
+ * @see CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT
+ * @see CameraCharacteristics#SENSOR_INFO_PHYSICAL_SIZE
+ * @see CameraCharacteristics#SENSOR_INFO_WHITE_LEVEL
+ * @see CameraCharacteristics#SENSOR_ORIENTATION
+ * @see CaptureResult#SENSOR_ROLLING_SHUTTER_SKEW
+ * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+ */
+ public static final int INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL = 4;
+
//
// Enumeration values for CameraCharacteristics#SYNC_MAX_LATENCY
//
@@ -1089,6 +1239,26 @@
public static final int SYNC_MAX_LATENCY_UNKNOWN = -1;
//
+ // Enumeration values for CameraCharacteristics#LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE
+ //
+
+ /**
+ * <p>A software mechanism is used to synchronize between the physical cameras. As a result,
+ * the timestamp of an image from a physical stream is only an approximation of the
+ * image sensor start-of-exposure time.</p>
+ * @see CameraCharacteristics#LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE
+ */
+ public static final int LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE_APPROXIMATE = 0;
+
+ /**
+ * <p>The camera device supports frame timestamp synchronization at the hardware level,
+ * and the timestamp of a physical stream image accurately reflects its
+ * start-of-exposure time.</p>
+ * @see CameraCharacteristics#LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE
+ */
+ public static final int LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE_CALIBRATED = 1;
+
+ //
// Enumeration values for CaptureRequest#COLOR_CORRECTION_MODE
//
@@ -1288,6 +1458,20 @@
*/
public static final int CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE = 4;
+ /**
+ * <p>An external flash has been turned on.</p>
+ * <p>It informs the camera device that an external flash has been turned on, and that
+ * metering (and continuous focus if active) should be quickly recaculated to account
+ * for the external flash. Otherwise, this mode acts like ON.</p>
+ * <p>When the external flash is turned off, AE mode should be changed to one of the
+ * other available AE modes.</p>
+ * <p>If the camera device supports AE external flash mode, aeState must be
+ * FLASH_REQUIRED after the camera device finishes AE scan and it's too dark without
+ * flash.</p>
+ * @see CaptureRequest#CONTROL_AE_MODE
+ */
+ public static final int CONTROL_AE_MODE_ON_EXTERNAL_FLASH = 5;
+
//
// Enumeration values for CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER
//
@@ -1661,6 +1845,16 @@
*/
public static final int CONTROL_CAPTURE_INTENT_MANUAL = 6;
+ /**
+ * <p>This request is for a motion tracking use case, where
+ * the application will use camera and inertial sensor data to
+ * locate and track objects in the world.</p>
+ * <p>The camera device auto-exposure routine will limit the exposure time
+ * of the camera to no more than 20 milliseconds, to minimize motion blur.</p>
+ * @see CaptureRequest#CONTROL_CAPTURE_INTENT
+ */
+ public static final int CONTROL_CAPTURE_INTENT_MOTION_TRACKING = 7;
+
//
// Enumeration values for CaptureRequest#CONTROL_EFFECT_MODE
//
@@ -2471,6 +2665,22 @@
public static final int STATISTICS_LENS_SHADING_MAP_MODE_ON = 1;
//
+ // Enumeration values for CaptureRequest#STATISTICS_OIS_DATA_MODE
+ //
+
+ /**
+ * <p>Do not include OIS data in the capture result.</p>
+ * @see CaptureRequest#STATISTICS_OIS_DATA_MODE
+ */
+ public static final int STATISTICS_OIS_DATA_MODE_OFF = 0;
+
+ /**
+ * <p>Include OIS data in the capture result.</p>
+ * @see CaptureRequest#STATISTICS_OIS_DATA_MODE
+ */
+ public static final int STATISTICS_OIS_DATA_MODE_ON = 1;
+
+ //
// Enumeration values for CaptureRequest#TONEMAP_MODE
//
diff --git a/android/hardware/camera2/CaptureRequest.java b/android/hardware/camera2/CaptureRequest.java
index 77da2a5..481b764 100644
--- a/android/hardware/camera2/CaptureRequest.java
+++ b/android/hardware/camera2/CaptureRequest.java
@@ -33,9 +33,11 @@
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
-
+import java.util.Set;
/**
* <p>An immutable package of settings and outputs needed to capture a single
@@ -219,7 +221,11 @@
private static final ArraySet<Surface> mEmptySurfaceSet = new ArraySet<Surface>();
- private final CameraMetadataNative mSettings;
+ private String mLogicalCameraId;
+ private CameraMetadataNative mLogicalCameraSettings;
+ private final HashMap<String, CameraMetadataNative> mPhysicalCameraSettings =
+ new HashMap<String, CameraMetadataNative>();
+
private boolean mIsReprocess;
// If this request is part of constrained high speed request list that was created by
// {@link android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession#createHighSpeedRequestList}
@@ -236,8 +242,6 @@
* Used by Binder to unparcel this object only.
*/
private CaptureRequest() {
- mSettings = new CameraMetadataNative();
- setNativeInstance(mSettings);
mIsReprocess = false;
mReprocessableSessionId = CameraCaptureSession.SESSION_ID_NONE;
}
@@ -249,8 +253,14 @@
*/
@SuppressWarnings("unchecked")
private CaptureRequest(CaptureRequest source) {
- mSettings = new CameraMetadataNative(source.mSettings);
- setNativeInstance(mSettings);
+ mLogicalCameraId = new String(source.mLogicalCameraId);
+ for (Map.Entry<String, CameraMetadataNative> entry :
+ source.mPhysicalCameraSettings.entrySet()) {
+ mPhysicalCameraSettings.put(new String(entry.getKey()),
+ new CameraMetadataNative(entry.getValue()));
+ }
+ mLogicalCameraSettings = mPhysicalCameraSettings.get(mLogicalCameraId);
+ setNativeInstance(mLogicalCameraSettings);
mSurfaceSet.addAll(source.mSurfaceSet);
mIsReprocess = source.mIsReprocess;
mIsPartOfCHSRequestList = source.mIsPartOfCHSRequestList;
@@ -272,16 +282,35 @@
* reprocess capture request to the same session where
* the {@link TotalCaptureResult}, used to create the reprocess
* capture, came from.
+ * @param logicalCameraId Camera Id of the actively open camera that instantiates the
+ * Builder.
+ *
+ * @param physicalCameraIdSet A set of physical camera ids that can be used to customize
+ * the request for a specific physical camera.
*
* @throws IllegalArgumentException If creating a reprocess capture request with an invalid
- * reprocessableSessionId.
+ * reprocessableSessionId, or multiple physical cameras.
*
* @see CameraDevice#createReprocessCaptureRequest
*/
private CaptureRequest(CameraMetadataNative settings, boolean isReprocess,
- int reprocessableSessionId) {
- mSettings = CameraMetadataNative.move(settings);
- setNativeInstance(mSettings);
+ int reprocessableSessionId, String logicalCameraId, Set<String> physicalCameraIdSet) {
+ if ((physicalCameraIdSet != null) && isReprocess) {
+ throw new IllegalArgumentException("Create a reprocess capture request with " +
+ "with more than one physical camera is not supported!");
+ }
+
+ mLogicalCameraId = logicalCameraId;
+ mLogicalCameraSettings = CameraMetadataNative.move(settings);
+ mPhysicalCameraSettings.put(mLogicalCameraId, mLogicalCameraSettings);
+ if (physicalCameraIdSet != null) {
+ for (String physicalId : physicalCameraIdSet) {
+ mPhysicalCameraSettings.put(physicalId, new CameraMetadataNative(
+ mLogicalCameraSettings));
+ }
+ }
+
+ setNativeInstance(mLogicalCameraSettings);
mIsReprocess = isReprocess;
if (isReprocess) {
if (reprocessableSessionId == CameraCaptureSession.SESSION_ID_NONE) {
@@ -309,7 +338,7 @@
*/
@Nullable
public <T> T get(Key<T> key) {
- return mSettings.get(key);
+ return mLogicalCameraSettings.get(key);
}
/**
@@ -319,7 +348,7 @@
@SuppressWarnings("unchecked")
@Override
protected <T> T getProtected(Key<?> key) {
- return (T) mSettings.get(key);
+ return (T) mLogicalCameraSettings.get(key);
}
/**
@@ -403,7 +432,7 @@
* @hide
*/
public CameraMetadataNative getNativeCopy() {
- return new CameraMetadataNative(mSettings);
+ return new CameraMetadataNative(mLogicalCameraSettings);
}
/**
@@ -444,14 +473,16 @@
return other != null
&& Objects.equals(mUserTag, other.mUserTag)
&& mSurfaceSet.equals(other.mSurfaceSet)
- && mSettings.equals(other.mSettings)
+ && mPhysicalCameraSettings.equals(other.mPhysicalCameraSettings)
+ && mLogicalCameraId.equals(other.mLogicalCameraId)
+ && mLogicalCameraSettings.equals(other.mLogicalCameraSettings)
&& mIsReprocess == other.mIsReprocess
&& mReprocessableSessionId == other.mReprocessableSessionId;
}
@Override
public int hashCode() {
- return HashCodeHelpers.hashCodeGeneric(mSettings, mSurfaceSet, mUserTag);
+ return HashCodeHelpers.hashCodeGeneric(mPhysicalCameraSettings, mSurfaceSet, mUserTag);
}
public static final Parcelable.Creator<CaptureRequest> CREATOR =
@@ -479,8 +510,25 @@
* @hide
*/
private void readFromParcel(Parcel in) {
- mSettings.readFromParcel(in);
- setNativeInstance(mSettings);
+ int physicalCameraCount = in.readInt();
+ if (physicalCameraCount <= 0) {
+ throw new RuntimeException("Physical camera count" + physicalCameraCount +
+ " should always be positive");
+ }
+
+ //Always start with the logical camera id
+ mLogicalCameraId = in.readString();
+ mLogicalCameraSettings = new CameraMetadataNative();
+ mLogicalCameraSettings.readFromParcel(in);
+ setNativeInstance(mLogicalCameraSettings);
+ mPhysicalCameraSettings.put(mLogicalCameraId, mLogicalCameraSettings);
+ for (int i = 1; i < physicalCameraCount; i++) {
+ String physicalId = in.readString();
+ CameraMetadataNative physicalCameraSettings = new CameraMetadataNative();
+ physicalCameraSettings.readFromParcel(in);
+ mPhysicalCameraSettings.put(physicalId, physicalCameraSettings);
+ }
+
mIsReprocess = (in.readInt() == 0) ? false : true;
mReprocessableSessionId = CameraCaptureSession.SESSION_ID_NONE;
@@ -509,7 +557,19 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
- mSettings.writeToParcel(dest, flags);
+ int physicalCameraCount = mPhysicalCameraSettings.size();
+ dest.writeInt(physicalCameraCount);
+ //Logical camera id and settings always come first.
+ dest.writeString(mLogicalCameraId);
+ mLogicalCameraSettings.writeToParcel(dest, flags);
+ for (Map.Entry<String, CameraMetadataNative> entry : mPhysicalCameraSettings.entrySet()) {
+ if (entry.getKey().equals(mLogicalCameraId)) {
+ continue;
+ }
+ dest.writeString(entry.getKey());
+ entry.getValue().writeToParcel(dest, flags);
+ }
+
dest.writeInt(mIsReprocess ? 1 : 0);
synchronized (mSurfacesLock) {
@@ -542,6 +602,14 @@
}
/**
+ * Retrieves the logical camera id.
+ * @hide
+ */
+ public String getLogicalCameraId() {
+ return mLogicalCameraId;
+ }
+
+ /**
* @hide
*/
public void convertSurfaceToStreamId(
@@ -633,14 +701,20 @@
* submits a reprocess capture request to the same session
* where the {@link TotalCaptureResult}, used to create the
* reprocess capture, came from.
+ * @param logicalCameraId Camera Id of the actively open camera that instantiates the
+ * Builder.
+ * @param physicalCameraIdSet A set of physical camera ids that can be used to customize
+ * the request for a specific physical camera.
*
* @throws IllegalArgumentException If creating a reprocess capture request with an invalid
* reprocessableSessionId.
* @hide
*/
public Builder(CameraMetadataNative template, boolean reprocess,
- int reprocessableSessionId) {
- mRequest = new CaptureRequest(template, reprocess, reprocessableSessionId);
+ int reprocessableSessionId, String logicalCameraId,
+ Set<String> physicalCameraIdSet) {
+ mRequest = new CaptureRequest(template, reprocess, reprocessableSessionId,
+ logicalCameraId, physicalCameraIdSet);
}
/**
@@ -682,7 +756,7 @@
* type to the key.
*/
public <T> void set(@NonNull Key<T> key, T value) {
- mRequest.mSettings.set(key, value);
+ mRequest.mLogicalCameraSettings.set(key, value);
}
/**
@@ -696,7 +770,71 @@
*/
@Nullable
public <T> T get(Key<T> key) {
- return mRequest.mSettings.get(key);
+ return mRequest.mLogicalCameraSettings.get(key);
+ }
+
+ /**
+ * Set a capture request field to a value. The field definitions can be
+ * found in {@link CaptureRequest}.
+ *
+ * <p>Setting a field to {@code null} will remove that field from the capture request.
+ * Unless the field is optional, removing it will likely produce an error from the camera
+ * device when the request is submitted.</p>
+ *
+ *<p>This method can be called for logical camera devices, which are devices that have
+ * REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA capability and calls to
+ * {@link CameraCharacteristics#getPhysicalCameraIds} return a non-empty list of
+ * physical devices that are backing the logical camera. The camera Id included in the
+ * 'physicalCameraId' argument selects an individual physical device that will receive
+ * the customized capture request field.</p>
+ *
+ * @throws IllegalArgumentException if the physical camera id is not valid
+ *
+ * @param key The metadata field to write.
+ * @param value The value to set the field to, which must be of a matching
+ * @param physicalCameraId A valid physical camera Id. The valid camera Ids can be obtained
+ * via calls to {@link CameraCharacteristics#getPhysicalCameraIds}.
+ * @return The builder object.
+ * type to the key.
+ */
+ public <T> Builder setPhysicalCameraKey(@NonNull Key<T> key, T value,
+ @NonNull String physicalCameraId) {
+ if (!mRequest.mPhysicalCameraSettings.containsKey(physicalCameraId)) {
+ throw new IllegalArgumentException("Physical camera id: " + physicalCameraId +
+ " is not valid!");
+ }
+
+ mRequest.mPhysicalCameraSettings.get(physicalCameraId).set(key, value);
+
+ return this;
+ }
+
+ /**
+ * Get a capture request field value for a specific physical camera Id. The field
+ * definitions can be found in {@link CaptureRequest}.
+ *
+ *<p>This method can be called for logical camera devices, which are devices that have
+ * REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA capability and calls to
+ * {@link CameraCharacteristics#getPhysicalCameraIds} return a non-empty list of
+ * physical devices that are backing the logical camera. The camera Id included in the
+ * 'physicalCameraId' argument selects an individual physical device and returns
+ * its specific capture request field.</p>
+ *
+ * @throws IllegalArgumentException if the key or physical camera id were not valid
+ *
+ * @param key The metadata field to read.
+ * @param physicalCameraId A valid physical camera Id. The valid camera Ids can be obtained
+ * via calls to {@link CameraCharacteristics#getPhysicalCameraIds}.
+ * @return The value of that key, or {@code null} if the field is not set.
+ */
+ @Nullable
+ public <T> T getPhysicalCameraKey(Key<T> key,@NonNull String physicalCameraId) {
+ if (!mRequest.mPhysicalCameraSettings.containsKey(physicalCameraId)) {
+ throw new IllegalArgumentException("Physical camera id: " + physicalCameraId +
+ " is not valid!");
+ }
+
+ return mRequest.mPhysicalCameraSettings.get(physicalCameraId).get(key);
}
/**
@@ -748,7 +886,7 @@
* @hide
*/
public boolean isEmpty() {
- return mRequest.mSettings.isEmpty();
+ return mRequest.mLogicalCameraSettings.isEmpty();
}
}
@@ -1076,6 +1214,7 @@
* <li>{@link #CONTROL_AE_MODE_ON_AUTO_FLASH ON_AUTO_FLASH}</li>
* <li>{@link #CONTROL_AE_MODE_ON_ALWAYS_FLASH ON_ALWAYS_FLASH}</li>
* <li>{@link #CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE ON_AUTO_FLASH_REDEYE}</li>
+ * <li>{@link #CONTROL_AE_MODE_ON_EXTERNAL_FLASH ON_EXTERNAL_FLASH}</li>
* </ul></p>
* <p><b>Available values for this device:</b><br>
* {@link CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES android.control.aeAvailableModes}</p>
@@ -1093,6 +1232,7 @@
* @see #CONTROL_AE_MODE_ON_AUTO_FLASH
* @see #CONTROL_AE_MODE_ON_ALWAYS_FLASH
* @see #CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE
+ * @see #CONTROL_AE_MODE_ON_EXTERNAL_FLASH
*/
@PublicKey
public static final Key<Integer> CONTROL_AE_MODE =
@@ -1487,10 +1627,13 @@
* strategy.</p>
* <p>This control (except for MANUAL) is only effective if
* <code>{@link CaptureRequest#CONTROL_MODE android.control.mode} != OFF</code> and any 3A routine is active.</p>
- * <p>ZERO_SHUTTER_LAG will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities}
- * contains PRIVATE_REPROCESSING or YUV_REPROCESSING. MANUAL will be supported if
- * {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains MANUAL_SENSOR. Other intent values are
- * always supported.</p>
+ * <p>All intents are supported by all devices, except that:
+ * * ZERO_SHUTTER_LAG will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+ * PRIVATE_REPROCESSING or YUV_REPROCESSING.
+ * * MANUAL will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+ * MANUAL_SENSOR.
+ * * MOTION_TRACKING will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+ * MOTION_TRACKING.</p>
* <p><b>Possible values:</b>
* <ul>
* <li>{@link #CONTROL_CAPTURE_INTENT_CUSTOM CUSTOM}</li>
@@ -1500,6 +1643,7 @@
* <li>{@link #CONTROL_CAPTURE_INTENT_VIDEO_SNAPSHOT VIDEO_SNAPSHOT}</li>
* <li>{@link #CONTROL_CAPTURE_INTENT_ZERO_SHUTTER_LAG ZERO_SHUTTER_LAG}</li>
* <li>{@link #CONTROL_CAPTURE_INTENT_MANUAL MANUAL}</li>
+ * <li>{@link #CONTROL_CAPTURE_INTENT_MOTION_TRACKING MOTION_TRACKING}</li>
* </ul></p>
* <p>This key is available on all devices.</p>
*
@@ -1512,6 +1656,7 @@
* @see #CONTROL_CAPTURE_INTENT_VIDEO_SNAPSHOT
* @see #CONTROL_CAPTURE_INTENT_ZERO_SHUTTER_LAG
* @see #CONTROL_CAPTURE_INTENT_MANUAL
+ * @see #CONTROL_CAPTURE_INTENT_MOTION_TRACKING
*/
@PublicKey
public static final Key<Integer> CONTROL_CAPTURE_INTENT =
@@ -2612,6 +2757,29 @@
new Key<Integer>("android.statistics.lensShadingMapMode", int.class);
/**
+ * <p>Whether the camera device outputs the OIS data in output
+ * result metadata.</p>
+ * <p>When set to ON,
+ * {@link CaptureResult#STATISTICS_OIS_TIMESTAMPS android.statistics.oisTimestamps}, android.statistics.oisShiftPixelX,
+ * android.statistics.oisShiftPixelY will provide OIS data in the output result metadata.</p>
+ * <p><b>Possible values:</b>
+ * <ul>
+ * <li>{@link #STATISTICS_OIS_DATA_MODE_OFF OFF}</li>
+ * <li>{@link #STATISTICS_OIS_DATA_MODE_ON ON}</li>
+ * </ul></p>
+ * <p><b>Available values for this device:</b><br>
+ * android.Statistics.info.availableOisDataModes</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ *
+ * @see CaptureResult#STATISTICS_OIS_TIMESTAMPS
+ * @see #STATISTICS_OIS_DATA_MODE_OFF
+ * @see #STATISTICS_OIS_DATA_MODE_ON
+ */
+ @PublicKey
+ public static final Key<Integer> STATISTICS_OIS_DATA_MODE =
+ new Key<Integer>("android.statistics.oisDataMode", int.class);
+
+ /**
* <p>Tonemapping / contrast / gamma curve for the blue
* channel, to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is
* CONTRAST_CURVE.</p>
diff --git a/android/hardware/camera2/CaptureResult.java b/android/hardware/camera2/CaptureResult.java
index 6d7b06f..d730fa8 100644
--- a/android/hardware/camera2/CaptureResult.java
+++ b/android/hardware/camera2/CaptureResult.java
@@ -691,6 +691,7 @@
* <li>{@link #CONTROL_AE_MODE_ON_AUTO_FLASH ON_AUTO_FLASH}</li>
* <li>{@link #CONTROL_AE_MODE_ON_ALWAYS_FLASH ON_ALWAYS_FLASH}</li>
* <li>{@link #CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE ON_AUTO_FLASH_REDEYE}</li>
+ * <li>{@link #CONTROL_AE_MODE_ON_EXTERNAL_FLASH ON_EXTERNAL_FLASH}</li>
* </ul></p>
* <p><b>Available values for this device:</b><br>
* {@link CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES android.control.aeAvailableModes}</p>
@@ -708,6 +709,7 @@
* @see #CONTROL_AE_MODE_ON_AUTO_FLASH
* @see #CONTROL_AE_MODE_ON_ALWAYS_FLASH
* @see #CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE
+ * @see #CONTROL_AE_MODE_ON_EXTERNAL_FLASH
*/
@PublicKey
public static final Key<Integer> CONTROL_AE_MODE =
@@ -877,7 +879,7 @@
* </tr>
* </tbody>
* </table>
- * <p>When {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is AE_MODE_ON_*:</p>
+ * <p>When {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is AE_MODE_ON*:</p>
* <table>
* <thead>
* <tr>
@@ -998,10 +1000,13 @@
* </tr>
* </tbody>
* </table>
+ * <p>If the camera device supports AE external flash mode (ON_EXTERNAL_FLASH is included in
+ * {@link CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES android.control.aeAvailableModes}), aeState must be FLASH_REQUIRED after the camera device
+ * finishes AE scan and it's too dark without flash.</p>
* <p>For the above table, the camera device may skip reporting any state changes that happen
* without application intervention (i.e. mode switch, trigger, locking). Any state that
* can be skipped in that manner is called a transient state.</p>
- * <p>For example, for above AE modes (AE_MODE_ON_*), in addition to the state transitions
+ * <p>For example, for above AE modes (AE_MODE_ON*), in addition to the state transitions
* listed in above table, it is also legal for the camera device to skip one or more
* transient states between two results. See below table for examples:</p>
* <table>
@@ -1072,6 +1077,7 @@
* Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
* {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
*
+ * @see CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES
* @see CaptureRequest#CONTROL_AE_LOCK
* @see CaptureRequest#CONTROL_AE_MODE
* @see CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER
@@ -1754,10 +1760,13 @@
* strategy.</p>
* <p>This control (except for MANUAL) is only effective if
* <code>{@link CaptureRequest#CONTROL_MODE android.control.mode} != OFF</code> and any 3A routine is active.</p>
- * <p>ZERO_SHUTTER_LAG will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities}
- * contains PRIVATE_REPROCESSING or YUV_REPROCESSING. MANUAL will be supported if
- * {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains MANUAL_SENSOR. Other intent values are
- * always supported.</p>
+ * <p>All intents are supported by all devices, except that:
+ * * ZERO_SHUTTER_LAG will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+ * PRIVATE_REPROCESSING or YUV_REPROCESSING.
+ * * MANUAL will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+ * MANUAL_SENSOR.
+ * * MOTION_TRACKING will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+ * MOTION_TRACKING.</p>
* <p><b>Possible values:</b>
* <ul>
* <li>{@link #CONTROL_CAPTURE_INTENT_CUSTOM CUSTOM}</li>
@@ -1767,6 +1776,7 @@
* <li>{@link #CONTROL_CAPTURE_INTENT_VIDEO_SNAPSHOT VIDEO_SNAPSHOT}</li>
* <li>{@link #CONTROL_CAPTURE_INTENT_ZERO_SHUTTER_LAG ZERO_SHUTTER_LAG}</li>
* <li>{@link #CONTROL_CAPTURE_INTENT_MANUAL MANUAL}</li>
+ * <li>{@link #CONTROL_CAPTURE_INTENT_MOTION_TRACKING MOTION_TRACKING}</li>
* </ul></p>
* <p>This key is available on all devices.</p>
*
@@ -1779,6 +1789,7 @@
* @see #CONTROL_CAPTURE_INTENT_VIDEO_SNAPSHOT
* @see #CONTROL_CAPTURE_INTENT_ZERO_SHUTTER_LAG
* @see #CONTROL_CAPTURE_INTENT_MANUAL
+ * @see #CONTROL_CAPTURE_INTENT_MOTION_TRACKING
*/
@PublicKey
public static final Key<Integer> CONTROL_CAPTURE_INTENT =
@@ -2192,8 +2203,6 @@
* significant illumination change, this value will be set to DETECTED for a single capture
* result. Otherwise the value will be NOT_DETECTED. The threshold for detection is similar
* to what would trigger a new passive focus scan to begin in CONTINUOUS autofocus modes.</p>
- * <p>afSceneChange may be DETECTED only if afMode is AF_MODE_CONTINUOUS_VIDEO or
- * AF_MODE_CONTINUOUS_PICTURE. In other AF modes, afSceneChange must be NOT_DETECTED.</p>
* <p>This key will be available if the camera device advertises this key via {@link android.hardware.camera2.CameraCharacteristics#getAvailableCaptureResultKeys }.</p>
* <p><b>Possible values:</b>
* <ul>
@@ -2761,36 +2770,33 @@
/**
* <p>Position of the camera optical center.</p>
* <p>The position of the camera device's lens optical center,
- * as a three-dimensional vector <code>(x,y,z)</code>, relative to the
- * optical center of the largest camera device facing in the
- * same direction as this camera, in the {@link android.hardware.SensorEvent Android sensor coordinate
- * axes}. Note that only the axis definitions are shared with
- * the sensor coordinate system, but not the origin.</p>
- * <p>If this device is the largest or only camera device with a
- * given facing, then this position will be <code>(0, 0, 0)</code>; a
- * camera device with a lens optical center located 3 cm from
- * the main sensor along the +X axis (to the right from the
- * user's perspective) will report <code>(0.03, 0, 0)</code>.</p>
- * <p>To transform a pixel coordinates between two cameras
- * facing the same direction, first the source camera
- * {@link CameraCharacteristics#LENS_RADIAL_DISTORTION android.lens.radialDistortion} must be corrected for. Then
- * the source camera {@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration} needs
- * to be applied, followed by the {@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation}
- * of the source camera, the translation of the source camera
- * relative to the destination camera, the
- * {@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation} of the destination camera, and
- * finally the inverse of {@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration}
- * of the destination camera. This obtains a
- * radial-distortion-free coordinate in the destination
- * camera pixel coordinates.</p>
- * <p>To compare this against a real image from the destination
- * camera, the destination camera image then needs to be
- * corrected for radial distortion before comparison or
- * sampling.</p>
+ * as a three-dimensional vector <code>(x,y,z)</code>.</p>
+ * <p>Prior to Android P, or when {@link CameraCharacteristics#LENS_POSE_REFERENCE android.lens.poseReference} is PRIMARY_CAMERA, this position
+ * is relative to the optical center of the largest camera device facing in the same
+ * direction as this camera, in the {@link android.hardware.SensorEvent Android sensor
+ * coordinate axes}. Note that only the axis definitions are shared with the sensor
+ * coordinate system, but not the origin.</p>
+ * <p>If this device is the largest or only camera device with a given facing, then this
+ * position will be <code>(0, 0, 0)</code>; a camera device with a lens optical center located 3 cm
+ * from the main sensor along the +X axis (to the right from the user's perspective) will
+ * report <code>(0.03, 0, 0)</code>.</p>
+ * <p>To transform a pixel coordinates between two cameras facing the same direction, first
+ * the source camera {@link CameraCharacteristics#LENS_RADIAL_DISTORTION android.lens.radialDistortion} must be corrected for. Then the source
+ * camera {@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration} needs to be applied, followed by the
+ * {@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation} of the source camera, the translation of the source camera
+ * relative to the destination camera, the {@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation} of the destination
+ * camera, and finally the inverse of {@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration} of the destination
+ * camera. This obtains a radial-distortion-free coordinate in the destination camera pixel
+ * coordinates.</p>
+ * <p>To compare this against a real image from the destination camera, the destination camera
+ * image then needs to be corrected for radial distortion before comparison or sampling.</p>
+ * <p>When {@link CameraCharacteristics#LENS_POSE_REFERENCE android.lens.poseReference} is GYROSCOPE, then this position is relative to
+ * the center of the primary gyroscope on the device.</p>
* <p><b>Units</b>: Meters</p>
* <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
*
* @see CameraCharacteristics#LENS_INTRINSIC_CALIBRATION
+ * @see CameraCharacteristics#LENS_POSE_REFERENCE
* @see CameraCharacteristics#LENS_POSE_ROTATION
* @see CameraCharacteristics#LENS_RADIAL_DISTORTION
*/
@@ -3903,6 +3909,76 @@
new Key<Integer>("android.statistics.lensShadingMapMode", int.class);
/**
+ * <p>Whether the camera device outputs the OIS data in output
+ * result metadata.</p>
+ * <p>When set to ON,
+ * {@link CaptureResult#STATISTICS_OIS_TIMESTAMPS android.statistics.oisTimestamps}, android.statistics.oisShiftPixelX,
+ * android.statistics.oisShiftPixelY will provide OIS data in the output result metadata.</p>
+ * <p><b>Possible values:</b>
+ * <ul>
+ * <li>{@link #STATISTICS_OIS_DATA_MODE_OFF OFF}</li>
+ * <li>{@link #STATISTICS_OIS_DATA_MODE_ON ON}</li>
+ * </ul></p>
+ * <p><b>Available values for this device:</b><br>
+ * android.Statistics.info.availableOisDataModes</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ *
+ * @see CaptureResult#STATISTICS_OIS_TIMESTAMPS
+ * @see #STATISTICS_OIS_DATA_MODE_OFF
+ * @see #STATISTICS_OIS_DATA_MODE_ON
+ */
+ @PublicKey
+ public static final Key<Integer> STATISTICS_OIS_DATA_MODE =
+ new Key<Integer>("android.statistics.oisDataMode", int.class);
+
+ /**
+ * <p>An array of timestamps of OIS samples, in nanoseconds.</p>
+ * <p>The array contains the timestamps of OIS samples. The timestamps are in the same
+ * timebase as and comparable to {@link CaptureResult#SENSOR_TIMESTAMP android.sensor.timestamp}.</p>
+ * <p><b>Units</b>: nanoseconds</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ *
+ * @see CaptureResult#SENSOR_TIMESTAMP
+ */
+ @PublicKey
+ public static final Key<long[]> STATISTICS_OIS_TIMESTAMPS =
+ new Key<long[]>("android.statistics.oisTimestamps", long[].class);
+
+ /**
+ * <p>An array of shifts of OIS samples, in x direction.</p>
+ * <p>The array contains the amount of shifts in x direction, in pixels, based on OIS samples.
+ * A positive value is a shift from left to right in active array coordinate system. For
+ * example, if the optical center is (1000, 500) in active array coordinates, an shift of
+ * (3, 0) puts the new optical center at (1003, 500).</p>
+ * <p>The number of shifts must match the number of timestamps in
+ * {@link CaptureResult#STATISTICS_OIS_TIMESTAMPS android.statistics.oisTimestamps}.</p>
+ * <p><b>Units</b>: Pixels in active array.</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ *
+ * @see CaptureResult#STATISTICS_OIS_TIMESTAMPS
+ */
+ @PublicKey
+ public static final Key<float[]> STATISTICS_OIS_X_SHIFTS =
+ new Key<float[]>("android.statistics.oisXShifts", float[].class);
+
+ /**
+ * <p>An array of shifts of OIS samples, in y direction.</p>
+ * <p>The array contains the amount of shifts in y direction, in pixels, based on OIS samples.
+ * A positive value is a shift from top to bottom in active array coordinate system. For
+ * example, if the optical center is (1000, 500) in active array coordinates, an shift of
+ * (0, 5) puts the new optical center at (1000, 505).</p>
+ * <p>The number of shifts must match the number of timestamps in
+ * {@link CaptureResult#STATISTICS_OIS_TIMESTAMPS android.statistics.oisTimestamps}.</p>
+ * <p><b>Units</b>: Pixels in active array.</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ *
+ * @see CaptureResult#STATISTICS_OIS_TIMESTAMPS
+ */
+ @PublicKey
+ public static final Key<float[]> STATISTICS_OIS_Y_SHIFTS =
+ new Key<float[]>("android.statistics.oisYShifts", float[].class);
+
+ /**
* <p>Tonemapping / contrast / gamma curve for the blue
* channel, to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is
* CONTRAST_CURVE.</p>
diff --git a/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java b/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java
index 8c4dbfa..06c2c25 100644
--- a/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java
+++ b/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java
@@ -94,8 +94,8 @@
// Note that after this step, the requestMetadata is mutated (swapped) and can not be used
// for next request builder creation.
CaptureRequest.Builder singleTargetRequestBuilder = new CaptureRequest.Builder(
- requestMetadata, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE);
-
+ requestMetadata, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE,
+ request.getLogicalCameraId(), /*physicalCameraIdSet*/ null);
// Carry over userTag, as native metadata doesn't have this field.
singleTargetRequestBuilder.setTag(request.getTag());
@@ -120,7 +120,8 @@
// CaptureRequest.Builder creation.
requestMetadata = new CameraMetadataNative(request.getNativeCopy());
doubleTargetRequestBuilder = new CaptureRequest.Builder(
- requestMetadata, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE);
+ requestMetadata, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE,
+ request.getLogicalCameraId(), /*physicalCameraIdSet*/null);
doubleTargetRequestBuilder.setTag(request.getTag());
doubleTargetRequestBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT,
CaptureRequest.CONTROL_CAPTURE_INTENT_VIDEO_RECORD);
diff --git a/android/hardware/camera2/impl/CameraDeviceImpl.java b/android/hardware/camera2/impl/CameraDeviceImpl.java
index f1ffb89..cab9d70 100644
--- a/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -16,27 +16,25 @@
package android.hardware.camera2.impl;
-import static android.hardware.camera2.CameraAccessException.CAMERA_IN_USE;
+import static com.android.internal.util.function.pooled.PooledLambda.obtainRunnable;
-import android.graphics.ImageFormat;
+import android.hardware.ICameraService;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
-import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.ICameraDeviceCallbacks;
import android.hardware.camera2.ICameraDeviceUser;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.InputConfiguration;
import android.hardware.camera2.params.OutputConfiguration;
-import android.hardware.camera2.params.ReprocessFormatsMap;
import android.hardware.camera2.params.SessionConfiguration;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.hardware.camera2.utils.SubmitInfo;
import android.hardware.camera2.utils.SurfaceUtils;
-import android.hardware.ICameraService;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
@@ -51,16 +49,15 @@
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
-import java.util.Collections;
-import java.util.concurrent.atomic.AtomicBoolean;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
-import java.util.List;
import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* HAL2.1+ implementation of CameraDevice. Use CameraManager#open to instantiate
@@ -715,6 +712,38 @@
}
@Override
+ public CaptureRequest.Builder createCaptureRequest(int templateType,
+ Set<String> physicalCameraIdSet)
+ throws CameraAccessException {
+ synchronized(mInterfaceLock) {
+ checkIfCameraClosedOrInError();
+
+ for (String physicalId : physicalCameraIdSet) {
+ if (physicalId == getId()) {
+ throw new IllegalStateException("Physical id matches the logical id!");
+ }
+ }
+
+ CameraMetadataNative templatedRequest = null;
+
+ templatedRequest = mRemoteDevice.createDefaultRequest(templateType);
+
+ // If app target SDK is older than O, or it's not a still capture template, enableZsl
+ // must be false in the default request.
+ if (mAppTargetSdkVersion < Build.VERSION_CODES.O ||
+ templateType != TEMPLATE_STILL_CAPTURE) {
+ overrideEnableZsl(templatedRequest, false);
+ }
+
+ CaptureRequest.Builder builder = new CaptureRequest.Builder(
+ templatedRequest, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE,
+ getId(), physicalCameraIdSet);
+
+ return builder;
+ }
+ }
+
+ @Override
public CaptureRequest.Builder createCaptureRequest(int templateType)
throws CameraAccessException {
synchronized(mInterfaceLock) {
@@ -732,7 +761,8 @@
}
CaptureRequest.Builder builder = new CaptureRequest.Builder(
- templatedRequest, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE);
+ templatedRequest, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE,
+ getId(), /*physicalCameraIdSet*/ null);
return builder;
}
@@ -748,7 +778,7 @@
CameraMetadataNative(inputResult.getNativeCopy());
return new CaptureRequest.Builder(resultMetadata, /*reprocess*/true,
- inputResult.getSessionId());
+ inputResult.getSessionId(), getId(), /*physicalCameraIdSet*/ null);
}
}
@@ -958,7 +988,8 @@
// callback is valid
handler = checkHandler(handler, callback);
- // Make sure that there all requests have at least 1 surface; all surfaces are non-null
+ // Make sure that there all requests have at least 1 surface; all surfaces are non-null;
+ // the surface isn't a physical stream surface for reprocessing request
for (CaptureRequest request : requestList) {
if (request.getTargets().isEmpty()) {
throw new IllegalArgumentException(
@@ -969,7 +1000,20 @@
if (surface == null) {
throw new IllegalArgumentException("Null Surface targets are not allowed");
}
+
+ if (!request.isReprocess()) {
+ continue;
+ }
+ for (int i = 0; i < mConfiguredOutputs.size(); i++) {
+ OutputConfiguration configuration = mConfiguredOutputs.valueAt(i);
+ if (configuration.isForPhysicalCamera()
+ && configuration.getSurfaces().contains(surface)) {
+ throw new IllegalArgumentException(
+ "Reprocess request on physical stream is not allowed");
+ }
+ }
}
+
}
synchronized(mInterfaceLock) {
@@ -1798,34 +1842,36 @@
case ERROR_CAMERA_DISCONNECTED:
CameraDeviceImpl.this.mDeviceHandler.post(mCallOnDisconnected);
break;
- default:
- Log.e(TAG, "Unknown error from camera device: " + errorCode);
- // no break
- case ERROR_CAMERA_DEVICE:
- case ERROR_CAMERA_SERVICE:
- mInError = true;
- final int publicErrorCode = (errorCode == ERROR_CAMERA_DEVICE) ?
- StateCallback.ERROR_CAMERA_DEVICE :
- StateCallback.ERROR_CAMERA_SERVICE;
- Runnable r = new Runnable() {
- @Override
- public void run() {
- if (!CameraDeviceImpl.this.isClosed()) {
- mDeviceCallback.onError(CameraDeviceImpl.this, publicErrorCode);
- }
- }
- };
- CameraDeviceImpl.this.mDeviceHandler.post(r);
- break;
case ERROR_CAMERA_REQUEST:
case ERROR_CAMERA_RESULT:
case ERROR_CAMERA_BUFFER:
onCaptureErrorLocked(errorCode, resultExtras);
break;
+ case ERROR_CAMERA_DEVICE:
+ scheduleNotifyError(StateCallback.ERROR_CAMERA_DEVICE);
+ break;
+ case ERROR_CAMERA_DISABLED:
+ scheduleNotifyError(StateCallback.ERROR_CAMERA_DISABLED);
+ break;
+ default:
+ Log.e(TAG, "Unknown error from camera device: " + errorCode);
+ scheduleNotifyError(StateCallback.ERROR_CAMERA_SERVICE);
}
}
}
+ private void scheduleNotifyError(int code) {
+ mInError = true;
+ CameraDeviceImpl.this.mDeviceHandler.post(obtainRunnable(
+ CameraDeviceCallbacks::notifyError, this, code));
+ }
+
+ private void notifyError(int code) {
+ if (!CameraDeviceImpl.this.isClosed()) {
+ mDeviceCallback.onError(CameraDeviceImpl.this, code);
+ }
+ }
+
@Override
public void onRepeatingRequestError(long lastFrameNumber, int repeatingRequestId) {
if (DEBUG) {
diff --git a/android/hardware/camera2/params/OutputConfiguration.java b/android/hardware/camera2/params/OutputConfiguration.java
index a85b5f7..f47cd66 100644
--- a/android/hardware/camera2/params/OutputConfiguration.java
+++ b/android/hardware/camera2/params/OutputConfiguration.java
@@ -31,13 +31,12 @@
import android.util.Size;
import android.view.Surface;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Collections;
-import java.util.ArrayList;
-
import static com.android.internal.util.Preconditions.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
/**
* A class for describing camera output, which contains a {@link Surface} and its specific
* configuration for creating capture session.
@@ -266,6 +265,7 @@
mConfiguredGenerationId = surface.getGenerationId();
mIsDeferredConfig = false;
mIsShared = false;
+ mPhysicalCameraId = null;
}
/**
@@ -319,6 +319,7 @@
mConfiguredGenerationId = 0;
mIsDeferredConfig = true;
mIsShared = false;
+ mPhysicalCameraId = null;
}
/**
@@ -348,8 +349,9 @@
* </ol>
*
* <p>To enable surface sharing, this function must be called before {@link
- * CameraDevice#createCaptureSessionByOutputConfigurations}. Calling this function after {@link
- * CameraDevice#createCaptureSessionByOutputConfigurations} has no effect.</p>
+ * CameraDevice#createCaptureSessionByOutputConfigurations} or {@link
+ * CameraDevice#createReprocessableCaptureSessionByConfigurations}. Calling this function after
+ * {@link CameraDevice#createCaptureSessionByOutputConfigurations} has no effect.</p>
*
* <p>Up to {@link #getMaxSharedSurfaceCount} surfaces can be shared for an OutputConfiguration.
* The supported surfaces for sharing must be of type SurfaceTexture, SurfaceView,
@@ -360,6 +362,44 @@
}
/**
+ * Set the id of the physical camera for this OutputConfiguration
+ *
+ * <p>In the case one logical camera is made up of multiple physical cameras, it could be
+ * desirable for the camera application to request streams from individual physical cameras.
+ * This call achieves it by mapping the OutputConfiguration to the physical camera id.</p>
+ *
+ * <p>The valid physical camera id can be queried by {@link
+ * android.hardware.camera2.CameraCharacteristics#getPhysicalCameraIds}.
+ * </p>
+ *
+ * <p>Passing in a null physicalCameraId means that the OutputConfiguration is for a logical
+ * stream.</p>
+ *
+ * <p>This function must be called before {@link
+ * CameraDevice#createCaptureSessionByOutputConfigurations} or {@link
+ * CameraDevice#createReprocessableCaptureSessionByConfigurations}. Calling this function
+ * after {@link CameraDevice#createCaptureSessionByOutputConfigurations} or {@link
+ * CameraDevice#createReprocessableCaptureSessionByConfigurations} has no effect.</p>
+ *
+ * <p>The surface belonging to a physical camera OutputConfiguration must not be used as input
+ * or output of a reprocessing request. </p>
+ */
+ public void setPhysicalCameraId(@Nullable String physicalCameraId) {
+ mPhysicalCameraId = physicalCameraId;
+ }
+
+ /**
+ * Check if this configuration is for a physical camera.
+ *
+ * <p>This returns true if the output configuration was for a physical camera making up a
+ * logical multi camera via {@link OutputConfiguration#setPhysicalCameraId}.</p>
+ * @hide
+ */
+ public boolean isForPhysicalCamera() {
+ return (mPhysicalCameraId != null);
+ }
+
+ /**
* Check if this configuration has deferred configuration.
*
* <p>This will return true if the output configuration was constructed with surface deferred by
@@ -487,6 +527,7 @@
this.mConfiguredGenerationId = other.mConfiguredGenerationId;
this.mIsDeferredConfig = other.mIsDeferredConfig;
this.mIsShared = other.mIsShared;
+ this.mPhysicalCameraId = other.mPhysicalCameraId;
}
/**
@@ -502,6 +543,7 @@
boolean isShared = source.readInt() == 1;
ArrayList<Surface> surfaces = new ArrayList<Surface>();
source.readTypedList(surfaces, Surface.CREATOR);
+ String physicalCameraId = source.readString();
checkArgumentInRange(rotation, ROTATION_0, ROTATION_270, "Rotation constant");
@@ -524,6 +566,7 @@
StreamConfigurationMap.imageFormatToDataspace(ImageFormat.PRIVATE);
mConfiguredGenerationId = 0;
}
+ mPhysicalCameraId = physicalCameraId;
}
/**
@@ -622,6 +665,7 @@
dest.writeInt(mIsDeferredConfig ? 1 : 0);
dest.writeInt(mIsShared ? 1 : 0);
dest.writeTypedList(mSurfaces);
+ dest.writeString(mPhysicalCameraId);
}
/**
@@ -675,13 +719,15 @@
if (mIsDeferredConfig) {
return HashCodeHelpers.hashCode(
mRotation, mConfiguredSize.hashCode(), mConfiguredFormat, mConfiguredDataspace,
- mSurfaceGroupId, mSurfaceType, mIsShared ? 1 : 0);
+ mSurfaceGroupId, mSurfaceType, mIsShared ? 1 : 0,
+ mPhysicalCameraId == null ? 0 : mPhysicalCameraId.hashCode());
}
return HashCodeHelpers.hashCode(
mRotation, mSurfaces.hashCode(), mConfiguredGenerationId,
mConfiguredSize.hashCode(), mConfiguredFormat,
- mConfiguredDataspace, mSurfaceGroupId, mIsShared ? 1 : 0);
+ mConfiguredDataspace, mSurfaceGroupId, mIsShared ? 1 : 0,
+ mPhysicalCameraId == null ? 0 : mPhysicalCameraId.hashCode());
}
private static final String TAG = "OutputConfiguration";
@@ -701,4 +747,6 @@
private final boolean mIsDeferredConfig;
// Flag indicating if this config has shared surfaces
private boolean mIsShared;
+ // The physical camera id that this output configuration is for.
+ private String mPhysicalCameraId;
}
diff --git a/android/hardware/display/BrightnessChangeEvent.java b/android/hardware/display/BrightnessChangeEvent.java
index 3003607..2301824 100644
--- a/android/hardware/display/BrightnessChangeEvent.java
+++ b/android/hardware/display/BrightnessChangeEvent.java
@@ -16,6 +16,8 @@
package android.hardware.display;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -23,51 +25,65 @@
* Data about a brightness settings change.
*
* {@see DisplayManager.getBrightnessEvents()}
- * TODO make this SystemAPI
* @hide
*/
+@SystemApi
+@TestApi
public final class BrightnessChangeEvent implements Parcelable {
/** Brightness in nits */
- public int brightness;
+ public final float brightness;
/** Timestamp of the change {@see System.currentTimeMillis()} */
- public long timeStamp;
+ public final long timeStamp;
/** Package name of focused activity when brightness was changed.
* This will be null if the caller of {@see DisplayManager.getBrightnessEvents()}
* does not have access to usage stats {@see UsageStatsManager} */
- public String packageName;
+ public final String packageName;
/** User id of of the user running when brightness was changed.
* @hide */
- public int userId;
+ public final int userId;
/** Lux values of recent sensor data */
- public float[] luxValues;
+ public final float[] luxValues;
/** Timestamps of the lux sensor readings {@see System.currentTimeMillis()} */
- public long[] luxTimestamps;
+ public final long[] luxTimestamps;
/** Most recent battery level when brightness was changed or Float.NaN */
- public float batteryLevel;
+ public final float batteryLevel;
/** Color filter active to provide night mode */
- public boolean nightMode;
+ public final boolean nightMode;
/** If night mode color filter is active this will be the temperature in kelvin */
- public int colorTemperature;
+ public final int colorTemperature;
- /** Brightness level before slider adjustment */
- public int lastBrightness;
+ /** Brightness le vel before slider adjustment */
+ public final float lastBrightness;
- public BrightnessChangeEvent() {
+ /** @hide */
+ private BrightnessChangeEvent(float brightness, long timeStamp, String packageName,
+ int userId, float[] luxValues, long[] luxTimestamps, float batteryLevel,
+ boolean nightMode, int colorTemperature, float lastBrightness) {
+ this.brightness = brightness;
+ this.timeStamp = timeStamp;
+ this.packageName = packageName;
+ this.userId = userId;
+ this.luxValues = luxValues;
+ this.luxTimestamps = luxTimestamps;
+ this.batteryLevel = batteryLevel;
+ this.nightMode = nightMode;
+ this.colorTemperature = colorTemperature;
+ this.lastBrightness = lastBrightness;
}
/** @hide */
- public BrightnessChangeEvent(BrightnessChangeEvent other) {
+ public BrightnessChangeEvent(BrightnessChangeEvent other, boolean redactPackage) {
this.brightness = other.brightness;
this.timeStamp = other.timeStamp;
- this.packageName = other.packageName;
+ this.packageName = redactPackage ? null : other.packageName;
this.userId = other.userId;
this.luxValues = other.luxValues;
this.luxTimestamps = other.luxTimestamps;
@@ -78,7 +94,7 @@
}
private BrightnessChangeEvent(Parcel source) {
- brightness = source.readInt();
+ brightness = source.readFloat();
timeStamp = source.readLong();
packageName = source.readString();
userId = source.readInt();
@@ -87,7 +103,7 @@
batteryLevel = source.readFloat();
nightMode = source.readBoolean();
colorTemperature = source.readInt();
- lastBrightness = source.readInt();
+ lastBrightness = source.readFloat();
}
public static final Creator<BrightnessChangeEvent> CREATOR =
@@ -107,7 +123,7 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(brightness);
+ dest.writeFloat(brightness);
dest.writeLong(timeStamp);
dest.writeString(packageName);
dest.writeInt(userId);
@@ -116,6 +132,87 @@
dest.writeFloat(batteryLevel);
dest.writeBoolean(nightMode);
dest.writeInt(colorTemperature);
- dest.writeInt(lastBrightness);
+ dest.writeFloat(lastBrightness);
+ }
+
+ /** @hide */
+ public static class Builder {
+ private float mBrightness;
+ private long mTimeStamp;
+ private String mPackageName;
+ private int mUserId;
+ private float[] mLuxValues;
+ private long[] mLuxTimestamps;
+ private float mBatteryLevel;
+ private boolean mNightMode;
+ private int mColorTemperature;
+ private float mLastBrightness;
+
+ /** {@see BrightnessChangeEvent#brightness} */
+ public Builder setBrightness(float brightness) {
+ mBrightness = brightness;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#timeStamp} */
+ public Builder setTimeStamp(long timeStamp) {
+ mTimeStamp = timeStamp;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#packageName} */
+ public Builder setPackageName(String packageName) {
+ mPackageName = packageName;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#userId} */
+ public Builder setUserId(int userId) {
+ mUserId = userId;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#luxValues} */
+ public Builder setLuxValues(float[] luxValues) {
+ mLuxValues = luxValues;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#luxTimestamps} */
+ public Builder setLuxTimestamps(long[] luxTimestamps) {
+ mLuxTimestamps = luxTimestamps;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#batteryLevel} */
+ public Builder setBatteryLevel(float batteryLevel) {
+ mBatteryLevel = batteryLevel;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#nightMode} */
+ public Builder setNightMode(boolean nightMode) {
+ mNightMode = nightMode;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#colorTemperature} */
+ public Builder setColorTemperature(int colorTemperature) {
+ mColorTemperature = colorTemperature;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#lastBrightness} */
+ public Builder setLastBrightness(float lastBrightness) {
+ mLastBrightness = lastBrightness;
+ return this;
+ }
+
+ /** Builds a BrightnessChangeEvent */
+ public BrightnessChangeEvent build() {
+ return new BrightnessChangeEvent(mBrightness, mTimeStamp,
+ mPackageName, mUserId, mLuxValues, mLuxTimestamps, mBatteryLevel,
+ mNightMode, mColorTemperature, mLastBrightness);
+ }
}
}
diff --git a/android/hardware/display/BrightnessConfiguration.java b/android/hardware/display/BrightnessConfiguration.java
index 6c3be81..67e97bf 100644
--- a/android/hardware/display/BrightnessConfiguration.java
+++ b/android/hardware/display/BrightnessConfiguration.java
@@ -16,6 +16,9 @@
package android.hardware.display;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Pair;
@@ -23,15 +26,20 @@
import com.android.internal.util.Preconditions;
import java.util.Arrays;
+import java.util.Objects;
/** @hide */
+@SystemApi
+@TestApi
public final class BrightnessConfiguration implements Parcelable {
private final float[] mLux;
private final float[] mNits;
+ private final String mDescription;
- private BrightnessConfiguration(float[] lux, float[] nits) {
+ private BrightnessConfiguration(float[] lux, float[] nits, String description) {
mLux = lux;
mNits = nits;
+ mDescription = description;
}
/**
@@ -47,10 +55,19 @@
return Pair.create(Arrays.copyOf(mLux, mLux.length), Arrays.copyOf(mNits, mNits.length));
}
+ /**
+ * Returns description string.
+ * @hide
+ */
+ public String getDescription() {
+ return mDescription;
+ }
+
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeFloatArray(mLux);
dest.writeFloatArray(mNits);
+ dest.writeString(mDescription);
}
@Override
@@ -68,7 +85,9 @@
}
sb.append("(").append(mLux[i]).append(", ").append(mNits[i]).append(")");
}
- sb.append("]}");
+ sb.append("], '");
+ sb.append(mDescription);
+ sb.append("'}");
return sb.toString();
}
@@ -77,6 +96,7 @@
int result = 1;
result = result * 31 + Arrays.hashCode(mLux);
result = result * 31 + Arrays.hashCode(mNits);
+ result = result * 31 + mDescription.hashCode();
return result;
}
@@ -89,16 +109,17 @@
return false;
}
final BrightnessConfiguration other = (BrightnessConfiguration) o;
- return Arrays.equals(mLux, other.mLux) && Arrays.equals(mNits, other.mNits);
+ return Arrays.equals(mLux, other.mLux) && Arrays.equals(mNits, other.mNits)
+ && Objects.equals(mDescription, other.mDescription);
}
public static final Creator<BrightnessConfiguration> CREATOR =
new Creator<BrightnessConfiguration>() {
public BrightnessConfiguration createFromParcel(Parcel in) {
- Builder builder = new Builder();
float[] lux = in.createFloatArray();
float[] nits = in.createFloatArray();
- builder.setCurve(lux, nits);
+ Builder builder = new Builder(lux, nits);
+ builder.setDescription(in.readString());
return builder.build();
}
@@ -113,6 +134,29 @@
public static class Builder {
private float[] mCurveLux;
private float[] mCurveNits;
+ private String mDescription;
+
+ /**
+ * STOPSHIP remove when app has stopped using this.
+ * @hide
+ */
+ public Builder() {
+ }
+
+ /**
+ * Constructs the builder with the control points for the brightness curve.
+ *
+ * Brightness curves must have strictly increasing ambient brightness values in lux and
+ * monotonically increasing display brightness values in nits. In addition, the initial
+ * control point must be 0 lux.
+ *
+ * @throws IllegalArgumentException if the initial control point is not at 0 lux.
+ * @throws IllegalArgumentException if the lux levels are not strictly increasing.
+ * @throws IllegalArgumentException if the nit levels are not monotonically increasing.
+ */
+ public Builder(float[] lux, float[] nits) {
+ setCurve(lux, nits);
+ }
/**
* Sets the control points for the brightness curve.
@@ -124,6 +168,9 @@
* @throws IllegalArgumentException if the initial control point is not at 0 lux.
* @throws IllegalArgumentException if the lux levels are not strictly increasing.
* @throws IllegalArgumentException if the nit levels are not monotonically increasing.
+ *
+ * STOPSHIP remove when app has stopped using this.
+ * @hide
*/
public Builder setCurve(float[] lux, float[] nits) {
Preconditions.checkNotNull(lux);
@@ -147,6 +194,17 @@
}
/**
+ * Set description of the brightness curve.
+ *
+ * @param description brief text describing the curve pushed. It maybe truncated
+ * and will not be displayed in the UI
+ */
+ public Builder setDescription(@Nullable String description) {
+ mDescription = description;
+ return this;
+ }
+
+ /**
* Builds the {@link BrightnessConfiguration}.
*
* A brightness curve <b>must</b> be set before calling this.
@@ -155,7 +213,7 @@
if (mCurveLux == null || mCurveNits == null) {
throw new IllegalStateException("A curve must be set!");
}
- return new BrightnessConfiguration(mCurveLux, mCurveNits);
+ return new BrightnessConfiguration(mCurveLux, mCurveNits, mDescription);
}
private static void checkMonotonic(float[] vals, boolean strictlyIncreasing, String name) {
diff --git a/android/hardware/display/DisplayManager.java b/android/hardware/display/DisplayManager.java
index 7de667d..4de4880 100644
--- a/android/hardware/display/DisplayManager.java
+++ b/android/hardware/display/DisplayManager.java
@@ -22,6 +22,7 @@
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.annotation.TestApi;
import android.app.KeyguardManager;
import android.content.Context;
import android.graphics.Point;
@@ -622,25 +623,23 @@
* Fetch {@link BrightnessChangeEvent}s.
* @hide until we make it a system api.
*/
+ @SystemApi
+ @TestApi
@RequiresPermission(Manifest.permission.BRIGHTNESS_SLIDER_USAGE)
public List<BrightnessChangeEvent> getBrightnessEvents() {
return mGlobal.getBrightnessEvents(mContext.getOpPackageName());
}
/**
- * @hide STOPSHIP - remove when adaptive brightness accepts curves.
- */
- public void setBrightness(int brightness) {
- mGlobal.setBrightness(brightness);
- }
-
- /**
* Sets the global display brightness configuration.
*
* @hide
*/
+ @SystemApi
+ @TestApi
+ @RequiresPermission(Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS)
public void setBrightnessConfiguration(BrightnessConfiguration c) {
- setBrightnessConfigurationForUser(c, UserHandle.myUserId());
+ setBrightnessConfigurationForUser(c, UserHandle.myUserId(), mContext.getPackageName());
}
/**
@@ -651,8 +650,37 @@
*
* @hide
*/
- public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userId) {
- mGlobal.setBrightnessConfigurationForUser(c, userId);
+ public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userId,
+ String packageName) {
+ mGlobal.setBrightnessConfigurationForUser(c, userId, packageName);
+ }
+
+ /**
+ * Temporarily sets the brightness of the display.
+ * <p>
+ * Requires the {@link android.Manifest.permission#CONTROL_DISPLAY_BRIGHTNESS} permission.
+ * </p>
+ *
+ * @param brightness The brightness value from 0 to 255.
+ *
+ * @hide Requires signature permission.
+ */
+ public void setTemporaryBrightness(int brightness) {
+ mGlobal.setTemporaryBrightness(brightness);
+ }
+
+ /**
+ * Temporarily sets the auto brightness adjustment factor.
+ * <p>
+ * Requires the {@link android.Manifest.permission#CONTROL_DISPLAY_BRIGHTNESS} permission.
+ * </p>
+ *
+ * @param adjustment The adjustment factor from -1.0 to 1.0.
+ *
+ * @hide Requires signature permission.
+ */
+ public void setTemporaryAutoBrightnessAdjustment(float adjustment) {
+ mGlobal.setTemporaryAutoBrightnessAdjustment(adjustment);
}
/**
diff --git a/android/hardware/display/DisplayManagerGlobal.java b/android/hardware/display/DisplayManagerGlobal.java
index bf4cc1d..2d5f5e0 100644
--- a/android/hardware/display/DisplayManagerGlobal.java
+++ b/android/hardware/display/DisplayManagerGlobal.java
@@ -476,25 +476,50 @@
}
/**
- * Set brightness but don't add a BrightnessChangeEvent
- * STOPSHIP remove when adaptive brightness accepts curves.
+ * Sets the global brightness configuration for a given user.
+ *
+ * @hide
*/
- public void setBrightness(int brightness) {
+ public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userId,
+ String packageName) {
try {
- mDm.setBrightness(brightness);
+ mDm.setBrightnessConfigurationForUser(c, userId, packageName);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
/**
- * Sets the global brightness configuration for a given user.
+ * Temporarily sets the brightness of the display.
+ * <p>
+ * Requires the {@link android.Manifest.permission#CONTROL_DISPLAY_BRIGHTNESS} permission.
+ * </p>
*
- * @hide
+ * @param brightness The brightness value from 0 to 255.
+ *
+ * @hide Requires signature permission.
*/
- public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userId) {
+ public void setTemporaryBrightness(int brightness) {
try {
- mDm.setBrightnessConfigurationForUser(c, userId);
+ mDm.setTemporaryBrightness(brightness);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Temporarily sets the auto brightness adjustment factor.
+ * <p>
+ * Requires the {@link android.Manifest.permission#CONTROL_DISPLAY_BRIGHTNESS} permission.
+ * </p>
+ *
+ * @param adjustment The adjustment factor from -1.0 to 1.0.
+ *
+ * @hide Requires signature permission.
+ */
+ public void setTemporaryAutoBrightnessAdjustment(float adjustment) {
+ try {
+ mDm.setTemporaryAutoBrightnessAdjustment(adjustment);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
diff --git a/android/hardware/display/DisplayManagerInternal.java b/android/hardware/display/DisplayManagerInternal.java
index cd551bd..1cfad4f 100644
--- a/android/hardware/display/DisplayManagerInternal.java
+++ b/android/hardware/display/DisplayManagerInternal.java
@@ -179,6 +179,11 @@
public abstract void persistBrightnessSliderEvents();
/**
+ * Notifies the display manager that resource overlays have changed.
+ */
+ public abstract void onOverlayChanged();
+
+ /**
* Describes the requested power state of the display.
*
* This object is intended to describe the general characteristics of the
@@ -209,18 +214,12 @@
// nearby, turning it off temporarily until the object is moved away.
public boolean useProximitySensor;
- // The desired screen brightness in the range 0 (minimum / off) to 255 (brightest).
- // The display power controller may choose to clamp the brightness.
- // When auto-brightness is enabled, this field should specify a nominal default
- // value to use while waiting for the light sensor to report enough data.
- public int screenBrightness;
+ // An override of the screen brightness. Set to -1 is used if there's no override.
+ public int screenBrightnessOverride;
- // The screen auto-brightness adjustment factor in the range -1 (dimmer) to 1 (brighter).
- public float screenAutoBrightnessAdjustment;
-
- // Set to true if screenBrightness and screenAutoBrightnessAdjustment were both
- // set by the user as opposed to being programmatically controlled by apps.
- public boolean brightnessSetByUser;
+ // An override of the screen auto-brightness adjustment factor in the range -1 (dimmer) to
+ // 1 (brighter). Set to Float.NaN if there's no override.
+ public float screenAutoBrightnessAdjustmentOverride;
// If true, enables automatic brightness control.
public boolean useAutoBrightness;
@@ -252,10 +251,10 @@
public DisplayPowerRequest() {
policy = POLICY_BRIGHT;
useProximitySensor = false;
- screenBrightness = PowerManager.BRIGHTNESS_ON;
- screenAutoBrightnessAdjustment = 0.0f;
- screenLowPowerBrightnessFactor = 0.5f;
+ screenBrightnessOverride = -1;
useAutoBrightness = false;
+ screenAutoBrightnessAdjustmentOverride = Float.NaN;
+ screenLowPowerBrightnessFactor = 0.5f;
blockScreenOn = false;
dozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT;
dozeScreenState = Display.STATE_UNKNOWN;
@@ -276,11 +275,10 @@
public void copyFrom(DisplayPowerRequest other) {
policy = other.policy;
useProximitySensor = other.useProximitySensor;
- screenBrightness = other.screenBrightness;
- screenAutoBrightnessAdjustment = other.screenAutoBrightnessAdjustment;
- screenLowPowerBrightnessFactor = other.screenLowPowerBrightnessFactor;
- brightnessSetByUser = other.brightnessSetByUser;
+ screenBrightnessOverride = other.screenBrightnessOverride;
useAutoBrightness = other.useAutoBrightness;
+ screenAutoBrightnessAdjustmentOverride = other.screenAutoBrightnessAdjustmentOverride;
+ screenLowPowerBrightnessFactor = other.screenLowPowerBrightnessFactor;
blockScreenOn = other.blockScreenOn;
lowPowerMode = other.lowPowerMode;
boostScreenBrightness = other.boostScreenBrightness;
@@ -298,12 +296,12 @@
return other != null
&& policy == other.policy
&& useProximitySensor == other.useProximitySensor
- && screenBrightness == other.screenBrightness
- && screenAutoBrightnessAdjustment == other.screenAutoBrightnessAdjustment
+ && screenBrightnessOverride == other.screenBrightnessOverride
+ && useAutoBrightness == other.useAutoBrightness
+ && floatEquals(screenAutoBrightnessAdjustmentOverride,
+ other.screenAutoBrightnessAdjustmentOverride)
&& screenLowPowerBrightnessFactor
== other.screenLowPowerBrightnessFactor
- && brightnessSetByUser == other.brightnessSetByUser
- && useAutoBrightness == other.useAutoBrightness
&& blockScreenOn == other.blockScreenOn
&& lowPowerMode == other.lowPowerMode
&& boostScreenBrightness == other.boostScreenBrightness
@@ -311,6 +309,10 @@
&& dozeScreenState == other.dozeScreenState;
}
+ private boolean floatEquals(float f1, float f2) {
+ return f1 == f2 || Float.isNaN(f1) && Float.isNaN(f2);
+ }
+
@Override
public int hashCode() {
return 0; // don't care
@@ -320,11 +322,11 @@
public String toString() {
return "policy=" + policyToString(policy)
+ ", useProximitySensor=" + useProximitySensor
- + ", screenBrightness=" + screenBrightness
- + ", screenAutoBrightnessAdjustment=" + screenAutoBrightnessAdjustment
- + ", screenLowPowerBrightnessFactor=" + screenLowPowerBrightnessFactor
- + ", brightnessSetByUser=" + brightnessSetByUser
+ + ", screenBrightnessOverride=" + screenBrightnessOverride
+ ", useAutoBrightness=" + useAutoBrightness
+ + ", screenAutoBrightnessAdjustmentOverride="
+ + screenAutoBrightnessAdjustmentOverride
+ + ", screenLowPowerBrightnessFactor=" + screenLowPowerBrightnessFactor
+ ", blockScreenOn=" + blockScreenOn
+ ", lowPowerMode=" + lowPowerMode
+ ", boostScreenBrightness=" + boostScreenBrightness
diff --git a/android/hardware/fingerprint/FingerprintDialog.java b/android/hardware/fingerprint/FingerprintDialog.java
new file mode 100644
index 0000000..6b7fab7
--- /dev/null
+++ b/android/hardware/fingerprint/FingerprintDialog.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.fingerprint;
+
+import static android.Manifest.permission.USE_FINGERPRINT;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.hardware.fingerprint.FingerprintManager.AuthenticationCallback;
+import android.hardware.fingerprint.FingerprintManager.AuthenticationResult;
+import android.hardware.fingerprint.IFingerprintDialogReceiver;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.text.TextUtils;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A class that manages a system-provided fingerprint dialog.
+ */
+public class FingerprintDialog {
+
+ /**
+ * @hide
+ */
+ public static final String KEY_TITLE = "title";
+ /**
+ * @hide
+ */
+ public static final String KEY_SUBTITLE = "subtitle";
+ /**
+ * @hide
+ */
+ public static final String KEY_DESCRIPTION = "description";
+ /**
+ * @hide
+ */
+ public static final String KEY_POSITIVE_TEXT = "positive_text";
+ /**
+ * @hide
+ */
+ public static final String KEY_NEGATIVE_TEXT = "negative_text";
+
+ /**
+ * Error/help message will show for this amount of time.
+ * For error messages, the dialog will also be dismissed after this amount of time.
+ * Error messages will be propagated back to the application via AuthenticationCallback
+ * after this amount of time.
+ * @hide
+ */
+ public static final int HIDE_DIALOG_DELAY = 3000; // ms
+ /**
+ * @hide
+ */
+ public static final int DISMISSED_REASON_POSITIVE = 1;
+
+ /**
+ * @hide
+ */
+ public static final int DISMISSED_REASON_NEGATIVE = 2;
+
+ /**
+ * @hide
+ */
+ public static final int DISMISSED_REASON_USER_CANCEL = 3;
+
+ private static class ButtonInfo {
+ Executor executor;
+ DialogInterface.OnClickListener listener;
+ ButtonInfo(Executor ex, DialogInterface.OnClickListener l) {
+ executor = ex;
+ listener = l;
+ }
+ }
+
+ /**
+ * A builder that collects arguments, to be shown on the system-provided fingerprint dialog.
+ **/
+ public static class Builder {
+ private final Bundle bundle;
+ private ButtonInfo positiveButtonInfo;
+ private ButtonInfo negativeButtonInfo;
+
+ /**
+ * Creates a builder for a fingerprint dialog.
+ */
+ public Builder() {
+ bundle = new Bundle();
+ }
+
+ /**
+ * Required: Set the title to display.
+ * @param title
+ * @return
+ */
+ public Builder setTitle(@NonNull CharSequence title) {
+ bundle.putCharSequence(KEY_TITLE, title);
+ return this;
+ }
+
+ /**
+ * Optional: Set the subtitle to display.
+ * @param subtitle
+ * @return
+ */
+ public Builder setSubtitle(@NonNull CharSequence subtitle) {
+ bundle.putCharSequence(KEY_SUBTITLE, subtitle);
+ return this;
+ }
+
+ /**
+ * Optional: Set the description to display.
+ * @param description
+ * @return
+ */
+ public Builder setDescription(@NonNull CharSequence description) {
+ bundle.putCharSequence(KEY_DESCRIPTION, description);
+ return this;
+ }
+
+ /**
+ * Optional: Set the text for the positive button. If not set, the positive button
+ * will not show.
+ * @param text
+ * @return
+ * @hide
+ */
+ public Builder setPositiveButton(@NonNull CharSequence text,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull DialogInterface.OnClickListener listener) {
+ if (TextUtils.isEmpty(text)) {
+ throw new IllegalArgumentException("Text must be set and non-empty");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Executor must not be null");
+ }
+ if (listener == null) {
+ throw new IllegalArgumentException("Listener must not be null");
+ }
+ bundle.putCharSequence(KEY_POSITIVE_TEXT, text);
+ positiveButtonInfo = new ButtonInfo(executor, listener);
+ return this;
+ }
+
+ /**
+ * Required: Set the text for the negative button.
+ * @param text
+ * @return
+ */
+ public Builder setNegativeButton(@NonNull CharSequence text,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull DialogInterface.OnClickListener listener) {
+ if (TextUtils.isEmpty(text)) {
+ throw new IllegalArgumentException("Text must be set and non-empty");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Executor must not be null");
+ }
+ if (listener == null) {
+ throw new IllegalArgumentException("Listener must not be null");
+ }
+ bundle.putCharSequence(KEY_NEGATIVE_TEXT, text);
+ negativeButtonInfo = new ButtonInfo(executor, listener);
+ return this;
+ }
+
+ /**
+ * Creates a {@link FingerprintDialog} with the arguments supplied to this builder.
+ * @param context
+ * @return a {@link FingerprintDialog}
+ * @throws IllegalArgumentException if any of the required fields are not set.
+ */
+ public FingerprintDialog build(Context context) {
+ final CharSequence title = bundle.getCharSequence(KEY_TITLE);
+ final CharSequence negative = bundle.getCharSequence(KEY_NEGATIVE_TEXT);
+
+ if (TextUtils.isEmpty(title)) {
+ throw new IllegalArgumentException("Title must be set and non-empty");
+ } else if (TextUtils.isEmpty(negative)) {
+ throw new IllegalArgumentException("Negative text must be set and non-empty");
+ }
+ return new FingerprintDialog(context, bundle, positiveButtonInfo, negativeButtonInfo);
+ }
+ }
+
+ private FingerprintManager mFingerprintManager;
+ private Bundle mBundle;
+ private ButtonInfo mPositiveButtonInfo;
+ private ButtonInfo mNegativeButtonInfo;
+
+ IFingerprintDialogReceiver mDialogReceiver = new IFingerprintDialogReceiver.Stub() {
+ @Override
+ public void onDialogDismissed(int reason) {
+ // Check the reason and invoke OnClickListener(s) if necessary
+ if (reason == DISMISSED_REASON_POSITIVE) {
+ mPositiveButtonInfo.executor.execute(() -> {
+ mPositiveButtonInfo.listener.onClick(null, DialogInterface.BUTTON_POSITIVE);
+ });
+ } else if (reason == DISMISSED_REASON_NEGATIVE) {
+ mNegativeButtonInfo.executor.execute(() -> {
+ mNegativeButtonInfo.listener.onClick(null, DialogInterface.BUTTON_NEGATIVE);
+ });
+ }
+ }
+ };
+
+ private FingerprintDialog(Context context, Bundle bundle,
+ ButtonInfo positiveButtonInfo, ButtonInfo negativeButtonInfo) {
+ mBundle = bundle;
+ mPositiveButtonInfo = positiveButtonInfo;
+ mNegativeButtonInfo = negativeButtonInfo;
+ mFingerprintManager = context.getSystemService(FingerprintManager.class);
+ }
+
+ /**
+ * This call warms up the fingerprint hardware, displays a system-provided dialog,
+ * and starts scanning for a fingerprint. It terminates when
+ * {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} is called, when
+ * {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is called,
+ * when {@link AuthenticationCallback#onAuthenticationFailed()} is called or when the user
+ * dismisses the system-provided dialog, at which point the crypto object becomes invalid.
+ * This operation can be canceled by using the provided cancel object. The application will
+ * receive authentication errors through {@link AuthenticationCallback}, and button events
+ * through the corresponding callback set in
+ * {@link Builder#setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)}.
+ * It is safe to reuse the {@link FingerprintDialog} object, and calling
+ * {@link FingerprintDialog#authenticate(CancellationSignal, Executor, AuthenticationCallback)}
+ * while an existing authentication attempt is occurring will stop the previous client and
+ * start a new authentication. The interrupted client will receive a cancelled notification
+ * through {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)}.
+ *
+ * @throws IllegalArgumentException if any of the arguments are null
+ *
+ * @param crypto object associated with the call
+ * @param cancel an object that can be used to cancel authentication
+ * @param executor an executor to handle callback events
+ * @param callback an object to receive authentication events
+ */
+ @RequiresPermission(USE_FINGERPRINT)
+ public void authenticate(@NonNull FingerprintManager.CryptoObject crypto,
+ @NonNull CancellationSignal cancel,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull FingerprintManager.AuthenticationCallback callback) {
+ mFingerprintManager.authenticate(crypto, cancel, mBundle, executor, mDialogReceiver,
+ callback);
+ }
+
+ /**
+ * This call warms up the fingerprint hardware, displays a system-provided dialog,
+ * and starts scanning for a fingerprint. It terminates when
+ * {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} is called, when
+ * {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is called,
+ * when {@link AuthenticationCallback#onAuthenticationFailed()} is called or when the user
+ * dismisses the system-provided dialog. This operation can be canceled by using the provided
+ * cancel object. The application will receive authentication errors through
+ * {@link AuthenticationCallback}, and button events through the corresponding callback set in
+ * {@link Builder#setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)}.
+ * It is safe to reuse the {@link FingerprintDialog} object, and calling
+ * {@link FingerprintDialog#authenticate(CancellationSignal, Executor, AuthenticationCallback)}
+ * while an existing authentication attempt is occurring will stop the previous client and
+ * start a new authentication. The interrupted client will receive a cancelled notification
+ * through {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)}.
+ *
+ * @throws IllegalArgumentException if any of the arguments are null
+ *
+ * @param cancel an object that can be used to cancel authentication
+ * @param executor an executor to handle callback events
+ * @param callback an object to receive authentication events
+ */
+ @RequiresPermission(USE_FINGERPRINT)
+ public void authenticate(@NonNull CancellationSignal cancel,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull FingerprintManager.AuthenticationCallback callback) {
+ mFingerprintManager.authenticate(cancel, mBundle, executor, mDialogReceiver, callback);
+ }
+}
diff --git a/android/hardware/fingerprint/FingerprintManager.java b/android/hardware/fingerprint/FingerprintManager.java
index 987718a..62d92c4 100644
--- a/android/hardware/fingerprint/FingerprintManager.java
+++ b/android/hardware/fingerprint/FingerprintManager.java
@@ -16,6 +16,11 @@
package android.hardware.fingerprint;
+import static android.Manifest.permission.INTERACT_ACROSS_USERS;
+import static android.Manifest.permission.MANAGE_FINGERPRINT;
+import static android.Manifest.permission.USE_FINGERPRINT;
+
+import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -23,6 +28,7 @@
import android.app.ActivityManager;
import android.content.Context;
import android.os.Binder;
+import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.CancellationSignal.OnCancelListener;
import android.os.Handler;
@@ -38,14 +44,11 @@
import java.security.Signature;
import java.util.List;
+import java.util.concurrent.Executor;
import javax.crypto.Cipher;
import javax.crypto.Mac;
-import static android.Manifest.permission.INTERACT_ACROSS_USERS;
-import static android.Manifest.permission.MANAGE_FINGERPRINT;
-import static android.Manifest.permission.USE_FINGERPRINT;
-
/**
* A class that coordinates access to the fingerprint hardware.
*/
@@ -204,6 +207,7 @@
private CryptoObject mCryptoObject;
private Fingerprint mRemovalFingerprint;
private Handler mHandler;
+ private Executor mExecutor;
private class OnEnrollCancelListener implements OnCancelListener {
@Override
@@ -505,7 +509,9 @@
}
/**
- * Per-user version
+ * Per-user version, see {@link FingerprintManager#authenticate(CryptoObject,
+ * CancellationSignal, int, AuthenticationCallback, Handler)}
+ * @param userId the user ID that the fingerprint hardware will authenticate for.
* @hide
*/
@RequiresPermission(USE_FINGERPRINT)
@@ -530,7 +536,7 @@
mCryptoObject = crypto;
long sessionId = crypto != null ? crypto.getOpId() : 0;
mService.authenticate(mToken, sessionId, userId, mServiceReceiver, flags,
- mContext.getOpPackageName());
+ mContext.getOpPackageName(), null /* bundle */, null /* receiver */);
} catch (RemoteException e) {
Log.w(TAG, "Remote exception while authenticating: ", e);
if (callback != null) {
@@ -543,6 +549,111 @@
}
/**
+ * Per-user version, see {@link FingerprintManager#authenticate(CryptoObject,
+ * CancellationSignal, Bundle, Executor, IFingerprintDialogReceiver, AuthenticationCallback)}
+ * @param userId the user ID that the fingerprint hardware will authenticate for.
+ */
+ private void authenticate(int userId,
+ @Nullable CryptoObject crypto,
+ @NonNull CancellationSignal cancel,
+ @NonNull Bundle bundle,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull IFingerprintDialogReceiver receiver,
+ @NonNull AuthenticationCallback callback) {
+ mCryptoObject = crypto;
+ if (cancel.isCanceled()) {
+ Log.w(TAG, "authentication already canceled");
+ return;
+ } else {
+ cancel.setOnCancelListener(new OnAuthenticationCancelListener(crypto));
+ }
+
+ if (mService != null) {
+ try {
+ mExecutor = executor;
+ mAuthenticationCallback = callback;
+ final long sessionId = crypto != null ? crypto.getOpId() : 0;
+ mService.authenticate(mToken, sessionId, userId, mServiceReceiver,
+ 0 /* flags */, mContext.getOpPackageName(), bundle, receiver);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Remote exception while authenticating", e);
+ mExecutor.execute(() -> {
+ callback.onAuthenticationError(FINGERPRINT_ERROR_HW_UNAVAILABLE,
+ getErrorString(FINGERPRINT_ERROR_HW_UNAVAILABLE, 0 /* vendorCode */));
+ });
+ }
+ }
+ }
+
+ /**
+ * Private method, see {@link FingerprintDialog#authenticate(CancellationSignal, Executor,
+ * AuthenticationCallback)}
+ * @param cancel
+ * @param executor
+ * @param callback
+ * @hide
+ */
+ public void authenticate(
+ @NonNull CancellationSignal cancel,
+ @NonNull Bundle bundle,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull IFingerprintDialogReceiver receiver,
+ @NonNull AuthenticationCallback callback) {
+ if (cancel == null) {
+ throw new IllegalArgumentException("Must supply a cancellation signal");
+ }
+ if (bundle == null) {
+ throw new IllegalArgumentException("Must supply a bundle");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Must supply an executor");
+ }
+ if (receiver == null) {
+ throw new IllegalArgumentException("Must supply a receiver");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("Must supply a calback");
+ }
+ authenticate(UserHandle.myUserId(), null, cancel, bundle, executor, receiver, callback);
+ }
+
+ /**
+ * Private method, see {@link FingerprintDialog#authenticate(CryptoObject, CancellationSignal,
+ * Executor, AuthenticationCallback)}
+ * @param crypto
+ * @param cancel
+ * @param executor
+ * @param callback
+ * @hide
+ */
+ public void authenticate(@NonNull CryptoObject crypto,
+ @NonNull CancellationSignal cancel,
+ @NonNull Bundle bundle,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull IFingerprintDialogReceiver receiver,
+ @NonNull AuthenticationCallback callback) {
+ if (crypto == null) {
+ throw new IllegalArgumentException("Must supply a crypto object");
+ }
+ if (cancel == null) {
+ throw new IllegalArgumentException("Must supply a cancellation signal");
+ }
+ if (bundle == null) {
+ throw new IllegalArgumentException("Must supply a bundle");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Must supply an executor");
+ }
+ if (receiver == null) {
+ throw new IllegalArgumentException("Must supply a receiver");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("Must supply a calback");
+ }
+ authenticate(UserHandle.myUserId(), crypto, cancel, bundle, executor, receiver, callback);
+ }
+
+ /**
* Request fingerprint enrollment. This call warms up the fingerprint hardware
* and starts scanning for fingerprints. Progress will be indicated by callbacks to the
* {@link EnrollmentCallback} object. It terminates when
@@ -929,64 +1040,64 @@
}
}
- private void sendErrorResult(long deviceId, int errMsgId, int vendorCode) {
- // emulate HAL 2.1 behavior and send real errMsgId
- final int clientErrMsgId = errMsgId == FINGERPRINT_ERROR_VENDOR
- ? (vendorCode + FINGERPRINT_ERROR_VENDOR_BASE) : errMsgId;
- if (mEnrollmentCallback != null) {
- mEnrollmentCallback.onEnrollmentError(clientErrMsgId,
- getErrorString(errMsgId, vendorCode));
- } else if (mAuthenticationCallback != null) {
- mAuthenticationCallback.onAuthenticationError(clientErrMsgId,
- getErrorString(errMsgId, vendorCode));
- } else if (mRemovalCallback != null) {
- mRemovalCallback.onRemovalError(mRemovalFingerprint, clientErrMsgId,
- getErrorString(errMsgId, vendorCode));
- } else if (mEnumerateCallback != null) {
- mEnumerateCallback.onEnumerateError(clientErrMsgId,
- getErrorString(errMsgId, vendorCode));
- }
- }
-
private void sendEnrollResult(Fingerprint fp, int remaining) {
if (mEnrollmentCallback != null) {
mEnrollmentCallback.onEnrollmentProgress(remaining);
}
}
-
- private void sendAuthenticatedSucceeded(Fingerprint fp, int userId) {
- if (mAuthenticationCallback != null) {
- final AuthenticationResult result =
- new AuthenticationResult(mCryptoObject, fp, userId);
- mAuthenticationCallback.onAuthenticationSucceeded(result);
- }
- }
-
- private void sendAuthenticatedFailed() {
- if (mAuthenticationCallback != null) {
- mAuthenticationCallback.onAuthenticationFailed();
- }
- }
-
- private void sendAcquiredResult(long deviceId, int acquireInfo, int vendorCode) {
- if (mAuthenticationCallback != null) {
- mAuthenticationCallback.onAuthenticationAcquired(acquireInfo);
- }
- final String msg = getAcquiredString(acquireInfo, vendorCode);
- if (msg == null) {
- return;
- }
- // emulate HAL 2.1 behavior and send real acquiredInfo
- final int clientInfo = acquireInfo == FINGERPRINT_ACQUIRED_VENDOR
- ? (vendorCode + FINGERPRINT_ACQUIRED_VENDOR_BASE) : acquireInfo;
- if (mEnrollmentCallback != null) {
- mEnrollmentCallback.onEnrollmentHelp(clientInfo, msg);
- } else if (mAuthenticationCallback != null) {
- mAuthenticationCallback.onAuthenticationHelp(clientInfo, msg);
- }
- }
};
+ private void sendAuthenticatedSucceeded(Fingerprint fp, int userId) {
+ if (mAuthenticationCallback != null) {
+ final AuthenticationResult result =
+ new AuthenticationResult(mCryptoObject, fp, userId);
+ mAuthenticationCallback.onAuthenticationSucceeded(result);
+ }
+ }
+
+ private void sendAuthenticatedFailed() {
+ if (mAuthenticationCallback != null) {
+ mAuthenticationCallback.onAuthenticationFailed();
+ }
+ }
+
+ private void sendAcquiredResult(long deviceId, int acquireInfo, int vendorCode) {
+ if (mAuthenticationCallback != null) {
+ mAuthenticationCallback.onAuthenticationAcquired(acquireInfo);
+ }
+ final String msg = getAcquiredString(acquireInfo, vendorCode);
+ if (msg == null) {
+ return;
+ }
+ // emulate HAL 2.1 behavior and send real acquiredInfo
+ final int clientInfo = acquireInfo == FINGERPRINT_ACQUIRED_VENDOR
+ ? (vendorCode + FINGERPRINT_ACQUIRED_VENDOR_BASE) : acquireInfo;
+ if (mEnrollmentCallback != null) {
+ mEnrollmentCallback.onEnrollmentHelp(clientInfo, msg);
+ } else if (mAuthenticationCallback != null) {
+ mAuthenticationCallback.onAuthenticationHelp(clientInfo, msg);
+ }
+ }
+
+ private void sendErrorResult(long deviceId, int errMsgId, int vendorCode) {
+ // emulate HAL 2.1 behavior and send real errMsgId
+ final int clientErrMsgId = errMsgId == FINGERPRINT_ERROR_VENDOR
+ ? (vendorCode + FINGERPRINT_ERROR_VENDOR_BASE) : errMsgId;
+ if (mEnrollmentCallback != null) {
+ mEnrollmentCallback.onEnrollmentError(clientErrMsgId,
+ getErrorString(errMsgId, vendorCode));
+ } else if (mAuthenticationCallback != null) {
+ mAuthenticationCallback.onAuthenticationError(clientErrMsgId,
+ getErrorString(errMsgId, vendorCode));
+ } else if (mRemovalCallback != null) {
+ mRemovalCallback.onRemovalError(mRemovalFingerprint, clientErrMsgId,
+ getErrorString(errMsgId, vendorCode));
+ } else if (mEnumerateCallback != null) {
+ mEnumerateCallback.onEnumerateError(clientErrMsgId,
+ getErrorString(errMsgId, vendorCode));
+ }
+ }
+
/**
* @hide
*/
@@ -1023,7 +1134,10 @@
}
}
- private String getErrorString(int errMsg, int vendorCode) {
+ /**
+ * @hide
+ */
+ public String getErrorString(int errMsg, int vendorCode) {
switch (errMsg) {
case FINGERPRINT_ERROR_UNABLE_TO_PROCESS:
return mContext.getString(
@@ -1043,6 +1157,9 @@
case FINGERPRINT_ERROR_LOCKOUT_PERMANENT:
return mContext.getString(
com.android.internal.R.string.fingerprint_error_lockout_permanent);
+ case FINGERPRINT_ERROR_USER_CANCELED:
+ return mContext.getString(
+ com.android.internal.R.string.fingerprint_error_user_canceled);
case FINGERPRINT_ERROR_VENDOR: {
String[] msgArray = mContext.getResources().getStringArray(
com.android.internal.R.array.fingerprint_error_vendor);
@@ -1055,7 +1172,10 @@
return null;
}
- private String getAcquiredString(int acquireInfo, int vendorCode) {
+ /**
+ * @hide
+ */
+ public String getAcquiredString(int acquireInfo, int vendorCode) {
switch (acquireInfo) {
case FINGERPRINT_ACQUIRED_GOOD:
return null;
@@ -1096,22 +1216,47 @@
@Override // binder call
public void onAcquired(long deviceId, int acquireInfo, int vendorCode) {
- mHandler.obtainMessage(MSG_ACQUIRED, acquireInfo, vendorCode, deviceId).sendToTarget();
+ if (mExecutor != null) {
+ mExecutor.execute(() -> {
+ sendAcquiredResult(deviceId, acquireInfo, vendorCode);
+ });
+ } else {
+ mHandler.obtainMessage(MSG_ACQUIRED, acquireInfo, vendorCode,
+ deviceId).sendToTarget();
+ }
}
@Override // binder call
public void onAuthenticationSucceeded(long deviceId, Fingerprint fp, int userId) {
- mHandler.obtainMessage(MSG_AUTHENTICATION_SUCCEEDED, userId, 0, fp).sendToTarget();
+ if (mExecutor != null) {
+ mExecutor.execute(() -> {
+ sendAuthenticatedSucceeded(fp, userId);
+ });
+ } else {
+ mHandler.obtainMessage(MSG_AUTHENTICATION_SUCCEEDED, userId, 0, fp).sendToTarget();
+ }
}
@Override // binder call
public void onAuthenticationFailed(long deviceId) {
- mHandler.obtainMessage(MSG_AUTHENTICATION_FAILED).sendToTarget();
+ if (mExecutor != null) {
+ mExecutor.execute(() -> {
+ sendAuthenticatedFailed();
+ });
+ } else {
+ mHandler.obtainMessage(MSG_AUTHENTICATION_FAILED).sendToTarget();
+ }
}
@Override // binder call
public void onError(long deviceId, int error, int vendorCode) {
- mHandler.obtainMessage(MSG_ERROR, error, vendorCode, deviceId).sendToTarget();
+ if (mExecutor != null) {
+ mExecutor.execute(() -> {
+ sendErrorResult(deviceId, error, vendorCode);
+ });
+ } else {
+ mHandler.obtainMessage(MSG_ERROR, error, vendorCode, deviceId).sendToTarget();
+ }
}
@Override // binder call
diff --git a/android/hardware/hdmi/HdmiTimerRecordSources.java b/android/hardware/hdmi/HdmiTimerRecordSources.java
index 6fe13ca..d7c2e1b 100644
--- a/android/hardware/hdmi/HdmiTimerRecordSources.java
+++ b/android/hardware/hdmi/HdmiTimerRecordSources.java
@@ -187,7 +187,6 @@
* Base class for time-related information.
* @hide
*/
- @SystemApi
/* package */ static class TimeUnit {
/* package */ final int mHour;
/* package */ final int mMinute;
diff --git a/android/hardware/location/ContextHubClient.java b/android/hardware/location/ContextHubClient.java
index 52527ed..0a21083 100644
--- a/android/hardware/location/ContextHubClient.java
+++ b/android/hardware/location/ContextHubClient.java
@@ -15,9 +15,13 @@
*/
package android.hardware.location;
+import android.annotation.NonNull;
import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.os.RemoteException;
+import com.android.internal.util.Preconditions;
+
import dalvik.system.CloseGuard;
import java.io.Closeable;
@@ -31,16 +35,12 @@
*
* @hide
*/
+@SystemApi
public class ContextHubClient implements Closeable {
/*
* The proxy to the client interface at the service.
*/
- private final IContextHubClient mClientProxy;
-
- /*
- * The callback interface associated with this client.
- */
- private final IContextHubClientCallback mCallbackInterface;
+ private IContextHubClient mClientProxy = null;
/*
* The Context Hub that this client is attached to.
@@ -51,20 +51,33 @@
private final AtomicBoolean mIsClosed = new AtomicBoolean(false);
- /* package */ ContextHubClient(
- IContextHubClient clientProxy, IContextHubClientCallback callback,
- ContextHubInfo hubInfo) {
- mClientProxy = clientProxy;
- mCallbackInterface = callback;
+ /* package */ ContextHubClient(ContextHubInfo hubInfo) {
mAttachedHub = hubInfo;
mCloseGuard.open("close");
}
/**
+ * Sets the proxy interface of the client at the service. This method should always be called
+ * by the ContextHubManager after the client is registered at the service, and should only be
+ * called once.
+ *
+ * @param clientProxy the proxy of the client at the service
+ */
+ /* package */ void setClientProxy(IContextHubClient clientProxy) {
+ Preconditions.checkNotNull(clientProxy, "IContextHubClient cannot be null");
+ if (mClientProxy != null) {
+ throw new IllegalStateException("Cannot change client proxy multiple times");
+ }
+
+ mClientProxy = clientProxy;
+ }
+
+ /**
* Returns the hub that this client is attached to.
*
* @return the ContextHubInfo of the attached hub
*/
+ @NonNull
public ContextHubInfo getAttachedHub() {
return mAttachedHub;
}
@@ -96,12 +109,16 @@
*
* @return the result of sending the message defined as in ContextHubTransaction.Result
*
+ * @throws NullPointerException if NanoAppMessage is null
+ *
* @see NanoAppMessage
* @see ContextHubTransaction.Result
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
@ContextHubTransaction.Result
- public int sendMessageToNanoApp(NanoAppMessage message) {
+ public int sendMessageToNanoApp(@NonNull NanoAppMessage message) {
+ Preconditions.checkNotNull(message, "NanoAppMessage cannot be null");
+
try {
return mClientProxy.sendMessageToNanoApp(message);
} catch (RemoteException e) {
diff --git a/android/hardware/location/ContextHubClientCallback.java b/android/hardware/location/ContextHubClientCallback.java
index ab19d54..cc2fe65 100644
--- a/android/hardware/location/ContextHubClientCallback.java
+++ b/android/hardware/location/ContextHubClientCallback.java
@@ -15,15 +15,20 @@
*/
package android.hardware.location;
+import android.annotation.SystemApi;
+
+import java.util.concurrent.Executor;
+
/**
* A class for {@link android.hardware.location.ContextHubClient ContextHubClient} to
* receive messages and life-cycle events from nanoapps in the Context Hub at which the client is
* attached to.
*
- * This callback is registered through the
- * {@link android.hardware.location.ContextHubManager#createClient() creation} of
- * {@link android.hardware.location.ContextHubClient ContextHubClient}. Callbacks are
- * invoked in the following ways:
+ * This callback is registered through the {@link
+ * android.hardware.location.ContextHubManager#createClient(
+ * ContextHubInfo, ContextHubClientCallback, Executor) creation} of
+ * {@link android.hardware.location.ContextHubClient ContextHubClient}. Callbacks are invoked in
+ * the following ways:
* 1) Messages from nanoapps delivered through onMessageFromNanoApp may either be broadcasted
* or targeted to a specific client.
* 2) Nanoapp or Context Hub events (the remaining callbacks) are broadcasted to all clients, and
@@ -31,6 +36,7 @@
*
* @hide
*/
+@SystemApi
public class ContextHubClientCallback {
/**
* Callback invoked when receiving a message from a nanoapp.
@@ -38,48 +44,56 @@
* The message contents of this callback may either be broadcasted or targeted to the
* client receiving the invocation.
*
+ * @param client the client that is associated with this callback
* @param message the message sent by the nanoapp
*/
- public void onMessageFromNanoApp(NanoAppMessage message) {}
+ public void onMessageFromNanoApp(ContextHubClient client, NanoAppMessage message) {}
/**
* Callback invoked when the attached Context Hub has reset.
+ *
+ * @param client the client that is associated with this callback
*/
- public void onHubReset() {}
+ public void onHubReset(ContextHubClient client) {}
/**
* Callback invoked when a nanoapp aborts at the attached Context Hub.
*
+ * @param client the client that is associated with this callback
* @param nanoAppId the ID of the nanoapp that had aborted
* @param abortCode the reason for nanoapp's abort, specific to each nanoapp
*/
- public void onNanoAppAborted(long nanoAppId, int abortCode) {}
+ public void onNanoAppAborted(ContextHubClient client, long nanoAppId, int abortCode) {}
/**
* Callback invoked when a nanoapp is loaded at the attached Context Hub.
*
+ * @param client the client that is associated with this callback
* @param nanoAppId the ID of the nanoapp that had been loaded
*/
- public void onNanoAppLoaded(long nanoAppId) {}
+ public void onNanoAppLoaded(ContextHubClient client, long nanoAppId) {}
/**
* Callback invoked when a nanoapp is unloaded from the attached Context Hub.
*
+ * @param client the client that is associated with this callback
* @param nanoAppId the ID of the nanoapp that had been unloaded
*/
- public void onNanoAppUnloaded(long nanoAppId) {}
+ public void onNanoAppUnloaded(ContextHubClient client, long nanoAppId) {}
/**
* Callback invoked when a nanoapp is enabled at the attached Context Hub.
*
+ * @param client the client that is associated with this callback
* @param nanoAppId the ID of the nanoapp that had been enabled
*/
- public void onNanoAppEnabled(long nanoAppId) {}
+ public void onNanoAppEnabled(ContextHubClient client, long nanoAppId) {}
/**
* Callback invoked when a nanoapp is disabled at the attached Context Hub.
*
+ * @param client the client that is associated with this callback
* @param nanoAppId the ID of the nanoapp that had been disabled
*/
- public void onNanoAppDisabled(long nanoAppId) {}
+ public void onNanoAppDisabled(ContextHubClient client, long nanoAppId) {}
}
diff --git a/android/hardware/location/ContextHubInfo.java b/android/hardware/location/ContextHubInfo.java
index c2b2800..36123e3 100644
--- a/android/hardware/location/ContextHubInfo.java
+++ b/android/hardware/location/ContextHubInfo.java
@@ -221,9 +221,6 @@
/**
* @return the CHRE platform ID as defined in chre/version.h
- *
- * TODO(b/67734082): Expose as public API
- * @hide
*/
public long getChrePlatformId() {
return mChrePlatformId;
@@ -231,9 +228,6 @@
/**
* @return the CHRE API's major version as defined in chre/version.h
- *
- * TODO(b/67734082): Expose as public API
- * @hide
*/
public byte getChreApiMajorVersion() {
return mChreApiMajorVersion;
@@ -241,9 +235,6 @@
/**
* @return the CHRE API's minor version as defined in chre/version.h
- *
- * TODO(b/67734082): Expose as public API
- * @hide
*/
public byte getChreApiMinorVersion() {
return mChreApiMinorVersion;
@@ -251,9 +242,6 @@
/**
* @return the CHRE patch version as defined in chre/version.h
- *
- * TODO(b/67734082): Expose as public API
- * @hide
*/
public short getChrePatchVersion() {
return mChrePatchVersion;
diff --git a/android/hardware/location/ContextHubManager.java b/android/hardware/location/ContextHubManager.java
index 4cea0ac..de13c81 100644
--- a/android/hardware/location/ContextHubManager.java
+++ b/android/hardware/location/ContextHubManager.java
@@ -17,6 +17,7 @@
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
@@ -30,6 +31,8 @@
import android.os.ServiceManager.ServiceNotFoundException;
import android.util.Log;
+import com.android.internal.util.Preconditions;
+
import java.util.List;
import java.util.concurrent.Executor;
@@ -59,7 +62,11 @@
/**
* An interface to receive asynchronous communication from the context hub.
+ *
+ * @deprecated Use the more refined {@link android.hardware.location.ContextHubClientCallback}
+ * instead for notification callbacks.
*/
+ @Deprecated
public abstract static class Callback {
protected Callback() {}
@@ -75,7 +82,7 @@
public abstract void onMessageReceipt(
int hubHandle,
int nanoAppHandle,
- ContextHubMessage message);
+ @NonNull ContextHubMessage message);
}
/**
@@ -98,8 +105,13 @@
/**
* Get a handle to all the context hubs in the system
+ *
* @return array of context hub handles
+ *
+ * @deprecated Use {@link #getContextHubs()} instead. The use of handles are deprecated in the
+ * new APIs.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
public int[] getContextHubHandles() {
try {
@@ -116,7 +128,11 @@
* @return ContextHubInfo Information about the requested context hub.
*
* @see ContextHubInfo
+ *
+ * @deprecated Use {@link #getContextHubs()} instead. The use of handles are deprecated in the
+ * new APIs.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
public ContextHubInfo getContextHubInfo(int hubHandle) {
try {
@@ -144,9 +160,12 @@
* -1 otherwise
*
* @see NanoApp
+ *
+ * @deprecated Use {@link #loadNanoApp(ContextHubInfo, NanoAppBinary)} instead.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public int loadNanoApp(int hubHandle, NanoApp app) {
+ public int loadNanoApp(int hubHandle, @NonNull NanoApp app) {
try {
return mService.loadNanoApp(hubHandle, app);
} catch (RemoteException e) {
@@ -168,7 +187,10 @@
*
* @return 0 if the command for unloading was sent to the context hub;
* -1 otherwise
+ *
+ * @deprecated Use {@link #unloadNanoApp(ContextHubInfo, long)} instead.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
public int unloadNanoApp(int nanoAppHandle) {
try {
@@ -199,13 +221,18 @@
* TODO(b/30943489): Have the returned NanoAppInstanceInfo contain the
* correct information.
*
- * @param nanoAppHandle handle of the nanoAppInstance
- * @return NanoAppInstanceInfo Information about the nano app instance.
+ * @param nanoAppHandle handle of the nanoapp instance
+ * @return NanoAppInstanceInfo the NanoAppInstanceInfo of the nanoapp, or null if the nanoapp
+ * does not exist
*
* @see NanoAppInstanceInfo
+ *
+ * @deprecated Use {@link #queryNanoApps(ContextHubInfo)} instead to explicitly query the hub
+ * for loaded nanoapps.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public NanoAppInstanceInfo getNanoAppInstanceInfo(int nanoAppHandle) {
+ @Nullable public NanoAppInstanceInfo getNanoAppInstanceInfo(int nanoAppHandle) {
try {
return mService.getNanoAppInstanceInfo(nanoAppHandle);
} catch (RemoteException e) {
@@ -222,9 +249,13 @@
* @see NanoAppFilter
*
* @return int[] Array of handles to any found nano apps
+ *
+ * @deprecated Use {@link #queryNanoApps(ContextHubInfo)} instead to explicitly query the hub
+ * for loaded nanoapps.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public int[] findNanoAppOnHub(int hubHandle, NanoAppFilter filter) {
+ @NonNull public int[] findNanoAppOnHub(int hubHandle, @NonNull NanoAppFilter filter) {
try {
return mService.findNanoAppOnHub(hubHandle, filter);
} catch (RemoteException e) {
@@ -250,9 +281,16 @@
* @see ContextHubMessage
*
* @return int 0 on success, -1 otherwise
+ *
+ * @deprecated Use {@link android.hardware.location.ContextHubClient#sendMessageToNanoApp(
+ * NanoAppMessage)} instead, after creating a
+ * {@link android.hardware.location.ContextHubClient} with
+ * {@link #createClient(ContextHubInfo, ContextHubClientCallback, Executor)}
+ * or {@link #createClient(ContextHubInfo, ContextHubClientCallback)}.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public int sendMessage(int hubHandle, int nanoAppHandle, ContextHubMessage message) {
+ public int sendMessage(int hubHandle, int nanoAppHandle, @NonNull ContextHubMessage message) {
try {
return mService.sendMessage(hubHandle, nanoAppHandle, message);
} catch (RemoteException e) {
@@ -266,11 +304,9 @@
* @return the list of ContextHubInfo objects
*
* @see ContextHubInfo
- *
- * @hide
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public List<ContextHubInfo> getContextHubs() {
+ @NonNull public List<ContextHubInfo> getContextHubs() {
try {
return mService.getContextHubs();
} catch (RemoteException e) {
@@ -342,13 +378,16 @@
*
* @return the ContextHubTransaction of the request
*
- * @see NanoAppBinary
+ * @throws NullPointerException if hubInfo or NanoAppBinary is null
*
- * @hide
+ * @see NanoAppBinary
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public ContextHubTransaction<Void> loadNanoApp(
- ContextHubInfo hubInfo, NanoAppBinary appBinary) {
+ @NonNull public ContextHubTransaction<Void> loadNanoApp(
+ @NonNull ContextHubInfo hubInfo, @NonNull NanoAppBinary appBinary) {
+ Preconditions.checkNotNull(hubInfo, "ContextHubInfo cannot be null");
+ Preconditions.checkNotNull(appBinary, "NanoAppBinary cannot be null");
+
ContextHubTransaction<Void> transaction =
new ContextHubTransaction<>(ContextHubTransaction.TYPE_LOAD_NANOAPP);
IContextHubTransactionCallback callback = createTransactionCallback(transaction);
@@ -370,10 +409,13 @@
*
* @return the ContextHubTransaction of the request
*
- * @hide
+ * @throws NullPointerException if hubInfo is null
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public ContextHubTransaction<Void> unloadNanoApp(ContextHubInfo hubInfo, long nanoAppId) {
+ @NonNull public ContextHubTransaction<Void> unloadNanoApp(
+ @NonNull ContextHubInfo hubInfo, long nanoAppId) {
+ Preconditions.checkNotNull(hubInfo, "ContextHubInfo cannot be null");
+
ContextHubTransaction<Void> transaction =
new ContextHubTransaction<>(ContextHubTransaction.TYPE_UNLOAD_NANOAPP);
IContextHubTransactionCallback callback = createTransactionCallback(transaction);
@@ -395,10 +437,13 @@
*
* @return the ContextHubTransaction of the request
*
- * @hide
+ * @throws NullPointerException if hubInfo is null
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public ContextHubTransaction<Void> enableNanoApp(ContextHubInfo hubInfo, long nanoAppId) {
+ @NonNull public ContextHubTransaction<Void> enableNanoApp(
+ @NonNull ContextHubInfo hubInfo, long nanoAppId) {
+ Preconditions.checkNotNull(hubInfo, "ContextHubInfo cannot be null");
+
ContextHubTransaction<Void> transaction =
new ContextHubTransaction<>(ContextHubTransaction.TYPE_ENABLE_NANOAPP);
IContextHubTransactionCallback callback = createTransactionCallback(transaction);
@@ -420,10 +465,13 @@
*
* @return the ContextHubTransaction of the request
*
- * @hide
+ * @throws NullPointerException if hubInfo is null
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public ContextHubTransaction<Void> disableNanoApp(ContextHubInfo hubInfo, long nanoAppId) {
+ @NonNull public ContextHubTransaction<Void> disableNanoApp(
+ @NonNull ContextHubInfo hubInfo, long nanoAppId) {
+ Preconditions.checkNotNull(hubInfo, "ContextHubInfo cannot be null");
+
ContextHubTransaction<Void> transaction =
new ContextHubTransaction<>(ContextHubTransaction.TYPE_DISABLE_NANOAPP);
IContextHubTransactionCallback callback = createTransactionCallback(transaction);
@@ -444,10 +492,13 @@
*
* @return the ContextHubTransaction of the request
*
- * @hide
+ * @throws NullPointerException if hubInfo is null
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public ContextHubTransaction<List<NanoAppState>> queryNanoApps(ContextHubInfo hubInfo) {
+ @NonNull public ContextHubTransaction<List<NanoAppState>> queryNanoApps(
+ @NonNull ContextHubInfo hubInfo) {
+ Preconditions.checkNotNull(hubInfo, "ContextHubInfo cannot be null");
+
ContextHubTransaction<List<NanoAppState>> transaction =
new ContextHubTransaction<>(ContextHubTransaction.TYPE_QUERY_NANOAPPS);
IContextHubTransactionCallback callback = createQueryCallback(transaction);
@@ -469,9 +520,14 @@
* @see Callback
*
* @return int 0 on success, -1 otherwise
+ *
+ * @deprecated Use {@link #createClient(ContextHubInfo, ContextHubClientCallback, Executor)}
+ * or {@link #createClient(ContextHubInfo, ContextHubClientCallback)} instead to
+ * register a {@link android.hardware.location.ContextHubClientCallback}.
*/
+ @Deprecated
@SuppressLint("Doclava125")
- public int registerCallback(Callback callback) {
+ public int registerCallback(@NonNull Callback callback) {
return registerCallback(callback, null);
}
@@ -498,7 +554,12 @@
* @see Callback
*
* @return int 0 on success, -1 otherwise
+ *
+ * @deprecated Use {@link #createClient(ContextHubInfo, ContextHubClientCallback, Executor)}
+ * or {@link #createClient(ContextHubInfo, ContextHubClientCallback)} instead to
+ * register a {@link android.hardware.location.ContextHubClientCallback}.
*/
+ @Deprecated
@SuppressLint("Doclava125")
public int registerCallback(Callback callback, Handler handler) {
synchronized(this) {
@@ -515,47 +576,48 @@
/**
* Creates an interface to the ContextHubClient to send down to the service.
*
+ * @param client the ContextHubClient object associated with this callback
* @param callback the callback to invoke at the client process
* @param executor the executor to invoke callbacks for this client
*
* @return the callback interface
*/
private IContextHubClientCallback createClientCallback(
- ContextHubClientCallback callback, Executor executor) {
+ ContextHubClient client, ContextHubClientCallback callback, Executor executor) {
return new IContextHubClientCallback.Stub() {
@Override
public void onMessageFromNanoApp(NanoAppMessage message) {
- executor.execute(() -> callback.onMessageFromNanoApp(message));
+ executor.execute(() -> callback.onMessageFromNanoApp(client, message));
}
@Override
public void onHubReset() {
- executor.execute(() -> callback.onHubReset());
+ executor.execute(() -> callback.onHubReset(client));
}
@Override
public void onNanoAppAborted(long nanoAppId, int abortCode) {
- executor.execute(() -> callback.onNanoAppAborted(nanoAppId, abortCode));
+ executor.execute(() -> callback.onNanoAppAborted(client, nanoAppId, abortCode));
}
@Override
public void onNanoAppLoaded(long nanoAppId) {
- executor.execute(() -> callback.onNanoAppLoaded(nanoAppId));
+ executor.execute(() -> callback.onNanoAppLoaded(client, nanoAppId));
}
@Override
public void onNanoAppUnloaded(long nanoAppId) {
- executor.execute(() -> callback.onNanoAppUnloaded(nanoAppId));
+ executor.execute(() -> callback.onNanoAppUnloaded(client, nanoAppId));
}
@Override
public void onNanoAppEnabled(long nanoAppId) {
- executor.execute(() -> callback.onNanoAppEnabled(nanoAppId));
+ executor.execute(() -> callback.onNanoAppEnabled(client, nanoAppId));
}
@Override
public void onNanoAppDisabled(long nanoAppId) {
- executor.execute(() -> callback.onNanoAppDisabled(nanoAppId));
+ executor.execute(() -> callback.onNanoAppDisabled(client, nanoAppId));
}
};
}
@@ -574,31 +636,30 @@
*
* @throws IllegalArgumentException if hubInfo does not represent a valid hub
* @throws IllegalStateException if there were too many registered clients at the service
- * @throws NullPointerException if callback or hubInfo is null
+ * @throws NullPointerException if callback, hubInfo, or executor is null
*
- * @hide
* @see ContextHubClientCallback
*/
@NonNull public ContextHubClient createClient(
@NonNull ContextHubInfo hubInfo, @NonNull ContextHubClientCallback callback,
@NonNull @CallbackExecutor Executor executor) {
- if (callback == null) {
- throw new NullPointerException("Callback cannot be null");
- }
- if (hubInfo == null) {
- throw new NullPointerException("Hub info cannot be null");
- }
+ Preconditions.checkNotNull(callback, "Callback cannot be null");
+ Preconditions.checkNotNull(hubInfo, "ContextHubInfo cannot be null");
+ Preconditions.checkNotNull(executor, "Executor cannot be null");
- IContextHubClientCallback clientInterface = createClientCallback(callback, executor);
+ ContextHubClient client = new ContextHubClient(hubInfo);
+ IContextHubClientCallback clientInterface = createClientCallback(
+ client, callback, executor);
- IContextHubClient client;
+ IContextHubClient clientProxy;
try {
- client = mService.createClient(clientInterface, hubInfo.getId());
+ clientProxy = mService.createClient(clientInterface, hubInfo.getId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
- return new ContextHubClient(client, clientInterface, hubInfo);
+ client.setClientProxy(clientProxy);
+ return client;
}
/**
@@ -612,7 +673,7 @@
* @throws IllegalArgumentException if hubInfo does not represent a valid hub
* @throws IllegalStateException if there were too many registered clients at the service
* @throws NullPointerException if callback or hubInfo is null
- * @hide
+ *
* @see ContextHubClientCallback
*/
@NonNull public ContextHubClient createClient(
@@ -628,9 +689,13 @@
* @param callback method to deregister
*
* @return int 0 on success, -1 otherwise
+ *
+ * @deprecated Use {@link android.hardware.location.ContextHubClient#close()} to unregister
+ * a {@link android.hardware.location.ContextHubClientCallback}.
*/
@SuppressLint("Doclava125")
- public int unregisterCallback(Callback callback) {
+ @Deprecated
+ public int unregisterCallback(@NonNull Callback callback) {
synchronized(this) {
if (callback != mCallback) {
Log.w(TAG, "Cannot recognize callback!");
@@ -679,8 +744,6 @@
synchronized (this) {
mLocalCallback.onMessageReceipt(hubId, nanoAppId, message);
}
- } else {
- Log.d(TAG, "Context hub manager client callback is NULL");
}
}
};
@@ -694,7 +757,7 @@
try {
mService.registerCallback(mClientCallback);
} catch (RemoteException e) {
- Log.w(TAG, "Could not register callback:" + e);
+ throw e.rethrowFromSystemServer();
}
}
}
diff --git a/android/hardware/location/ContextHubMessage.java b/android/hardware/location/ContextHubMessage.java
index bca2ae6..f85ce3e 100644
--- a/android/hardware/location/ContextHubMessage.java
+++ b/android/hardware/location/ContextHubMessage.java
@@ -19,22 +19,26 @@
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
-import android.util.Log;
import java.util.Arrays;
/**
+ * @deprecated Use {@link android.hardware.location.NanoAppMessage} instead to send messages with
+ * {@link android.hardware.location.ContextHubClient#sendMessageToNanoApp(
+ * NanoAppMessage)} and receive messages with
+ * {@link android.hardware.location.ContextHubClientCallback#onMessageFromNanoApp(
+ * ContextHubClient, NanoAppMessage)}.
+ *
* @hide
*/
@SystemApi
+@Deprecated
public class ContextHubMessage {
+ private static final int DEBUG_LOG_NUM_BYTES = 16;
private int mType;
private int mVersion;
private byte[]mData;
- private static final String TAG = "ContextHubMessage";
-
-
/**
* Get the message type
*
@@ -131,4 +135,28 @@
return new ContextHubMessage[size];
}
};
+
+ @Override
+ public String toString() {
+ int length = mData.length;
+
+ String ret =
+ "ContextHubMessage[type = " + mType + ", length = " + mData.length + " bytes](";
+ if (length > 0) {
+ ret += "data = 0x";
+ }
+ for (int i = 0; i < Math.min(length, DEBUG_LOG_NUM_BYTES); i++) {
+ ret += Byte.toHexString(mData[i], true /* upperCase */);
+
+ if ((i + 1) % 4 == 0) {
+ ret += " ";
+ }
+ }
+ if (length > DEBUG_LOG_NUM_BYTES) {
+ ret += "...";
+ }
+ ret += ")";
+
+ return ret;
+ }
}
diff --git a/android/hardware/location/ContextHubTransaction.java b/android/hardware/location/ContextHubTransaction.java
index a1b743d..bc7efef 100644
--- a/android/hardware/location/ContextHubTransaction.java
+++ b/android/hardware/location/ContextHubTransaction.java
@@ -18,9 +18,12 @@
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.os.Handler;
import android.os.HandlerExecutor;
+import com.android.internal.util.Preconditions;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.CountDownLatch;
@@ -35,17 +38,19 @@
* through the ContextHubManager APIs. The caller can either retrieve the result
* synchronously through a blocking call ({@link #waitForResponse(long, TimeUnit)}) or
* asynchronously through a user-defined listener
- * ({@link #setOnCompleteListener(Listener, Executor)} )}).
+ * ({@link #setOnCompleteListener(OnCompleteListener, Executor)} )}).
*
* @param <T> the type of the contents in the transaction response
*
* @hide
*/
+@SystemApi
public class ContextHubTransaction<T> {
private static final String TAG = "ContextHubTransaction";
/**
* Constants describing the type of a transaction through the Context Hub Service.
+ * {@hide}
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = { "TYPE_" }, value = {
@@ -65,6 +70,7 @@
/**
* Constants describing the result of a transaction or request through the Context Hub Service.
+ * {@hide}
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = { "RESULT_" }, value = {
@@ -72,7 +78,7 @@
RESULT_FAILED_UNKNOWN,
RESULT_FAILED_BAD_PARAMS,
RESULT_FAILED_UNINITIALIZED,
- RESULT_FAILED_PENDING,
+ RESULT_FAILED_BUSY,
RESULT_FAILED_AT_HUB,
RESULT_FAILED_TIMEOUT,
RESULT_FAILED_SERVICE_INTERNAL_FAILURE,
@@ -95,7 +101,7 @@
/**
* Failure mode when there are too many transactions pending.
*/
- public static final int RESULT_FAILED_PENDING = 4;
+ public static final int RESULT_FAILED_BUSY = 4;
/**
* Failure mode when the request went through, but failed asynchronously at the hub.
*/
@@ -151,7 +157,7 @@
* @param <L> the type of the contents in the transaction response
*/
@FunctionalInterface
- public interface Listener<L> {
+ public interface OnCompleteListener<L> {
/**
* The listener function to invoke when the transaction completes.
*
@@ -181,7 +187,7 @@
/*
* The listener to be invoked when the transaction completes.
*/
- private ContextHubTransaction.Listener<T> mListener = null;
+ private ContextHubTransaction.OnCompleteListener<T> mListener = null;
/*
* Synchronization latch used to block on response.
@@ -272,8 +278,8 @@
* A transaction can be invalidated if the process owning the transaction is no longer active
* and the reference to this object is lost.
*
- * This method or {@link #setOnCompleteListener(ContextHubTransaction.Listener)} can only be
- * invoked once, or an IllegalStateException will be thrown.
+ * This method or {@link #setOnCompleteListener(ContextHubTransaction.OnCompleteListener)} can
+ * only be invoked once, or an IllegalStateException will be thrown.
*
* @param listener the listener to be invoked upon completion
* @param executor the executor to invoke the callback
@@ -282,15 +288,11 @@
* @throws NullPointerException if the callback or handler is null
*/
public void setOnCompleteListener(
- @NonNull ContextHubTransaction.Listener<T> listener,
+ @NonNull ContextHubTransaction.OnCompleteListener<T> listener,
@NonNull @CallbackExecutor Executor executor) {
synchronized (this) {
- if (listener == null) {
- throw new NullPointerException("Listener cannot be null");
- }
- if (executor == null) {
- throw new NullPointerException("Executor cannot be null");
- }
+ Preconditions.checkNotNull(listener, "OnCompleteListener cannot be null");
+ Preconditions.checkNotNull(executor, "Executor cannot be null");
if (mListener != null) {
throw new IllegalStateException(
"Cannot set ContextHubTransaction listener multiple times");
@@ -308,18 +310,19 @@
/**
* Sets the listener to be invoked invoked when the transaction completes.
*
- * Equivalent to {@link #setOnCompleteListener(ContextHubTransaction.Listener, Executor)}
- * with the executor using the main thread's Looper.
+ * Equivalent to {@link #setOnCompleteListener(ContextHubTransaction.OnCompleteListener,
+ * Executor)} with the executor using the main thread's Looper.
*
- * This method or {@link #setOnCompleteListener(ContextHubTransaction.Listener, Executor)}
- * can only be invoked once, or an IllegalStateException will be thrown.
+ * This method or {@link #setOnCompleteListener(ContextHubTransaction.OnCompleteListener,
+ * Executor)} can only be invoked once, or an IllegalStateException will be thrown.
*
* @param listener the listener to be invoked upon completion
*
* @throws IllegalStateException if this method is called multiple times
* @throws NullPointerException if the callback is null
*/
- public void setOnCompleteListener(@NonNull ContextHubTransaction.Listener<T> listener) {
+ public void setOnCompleteListener(
+ @NonNull ContextHubTransaction.OnCompleteListener<T> listener) {
setOnCompleteListener(listener, new HandlerExecutor(Handler.getMain()));
}
@@ -337,9 +340,7 @@
*/
/* package */ void setResponse(ContextHubTransaction.Response<T> response) {
synchronized (this) {
- if (response == null) {
- throw new NullPointerException("Response cannot be null");
- }
+ Preconditions.checkNotNull(response, "Response cannot be null");
if (mIsResponseSet) {
throw new IllegalStateException(
"Cannot set response of ContextHubTransaction multiple times");
diff --git a/android/hardware/location/NanoApp.java b/android/hardware/location/NanoApp.java
index 0465def..b5c01ec 100644
--- a/android/hardware/location/NanoApp.java
+++ b/android/hardware/location/NanoApp.java
@@ -28,9 +28,14 @@
* Nano apps are expected to be used only by bundled apps only
* at this time.
*
+ * @deprecated Use {@link android.hardware.location.NanoAppBinary} instead to load a nanoapp with
+ * {@link android.hardware.location.ContextHubManager#loadNanoApp(
+ * ContextHubInfo, NanoAppBinary)}.
+ *
* @hide
*/
@SystemApi
+@Deprecated
public class NanoApp {
private final String TAG = "NanoApp";
diff --git a/android/hardware/location/NanoAppBinary.java b/android/hardware/location/NanoAppBinary.java
index 934e9e4..ba01ca2 100644
--- a/android/hardware/location/NanoAppBinary.java
+++ b/android/hardware/location/NanoAppBinary.java
@@ -15,6 +15,7 @@
*/
package android.hardware.location;
+import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
@@ -27,6 +28,7 @@
/**
* @hide
*/
+@SystemApi
public final class NanoAppBinary implements Parcelable {
private static final String TAG = "NanoAppBinary";
diff --git a/android/hardware/location/NanoAppFilter.java b/android/hardware/location/NanoAppFilter.java
index 5ccf546..75a96ee 100644
--- a/android/hardware/location/NanoAppFilter.java
+++ b/android/hardware/location/NanoAppFilter.java
@@ -16,15 +16,18 @@
package android.hardware.location;
-
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
/**
+ * @deprecated Use {@link android.hardware.location.ContextHubManager#queryNanoApps(ContextHubInfo)}
+ * to find loaded nanoapps, which doesn't require using this class as a parameter.
+ *
* @hide
*/
@SystemApi
+@Deprecated
public class NanoAppFilter {
private static final String TAG = "NanoAppFilter";
diff --git a/android/hardware/location/NanoAppInstanceInfo.java b/android/hardware/location/NanoAppInstanceInfo.java
index f73fd87..f1926ea 100644
--- a/android/hardware/location/NanoAppInstanceInfo.java
+++ b/android/hardware/location/NanoAppInstanceInfo.java
@@ -28,9 +28,12 @@
*
* TODO(b/69270990) Remove this class once the old API is deprecated.
*
+ * @deprecated Use {@link android.hardware.location.NanoAppState} instead.
+ *
* @hide
*/
@SystemApi
+@Deprecated
public class NanoAppInstanceInfo {
private String mPublisher = "Unknown";
private String mName = "Unknown";
@@ -90,11 +93,6 @@
/**
* Get the application version
*
- * NOTE: There is a race condition where shortly after loading, this
- * may return -1 instead of the correct version.
- *
- * TODO(b/30970527): Fix this race condition.
- *
* @return int - version of the app
*/
public int getAppVersion() {
diff --git a/android/hardware/location/NanoAppMessage.java b/android/hardware/location/NanoAppMessage.java
index 2028674..6635258 100644
--- a/android/hardware/location/NanoAppMessage.java
+++ b/android/hardware/location/NanoAppMessage.java
@@ -15,6 +15,7 @@
*/
package android.hardware.location;
+import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -25,7 +26,9 @@
*
* @hide
*/
+@SystemApi
public final class NanoAppMessage implements Parcelable {
+ private static final int DEBUG_LOG_NUM_BYTES = 16;
private long mNanoAppId;
private int mMessageType;
private byte[] mMessageBody;
@@ -140,4 +143,29 @@
return new NanoAppMessage[size];
}
};
+
+ @Override
+ public String toString() {
+ int length = mMessageBody.length;
+
+ String ret = "NanoAppMessage[type = " + mMessageType + ", length = " + mMessageBody.length
+ + " bytes, " + (mIsBroadcasted ? "broadcast" : "unicast") + ", nanoapp = 0x"
+ + Long.toHexString(mNanoAppId) + "](";
+ if (length > 0) {
+ ret += "data = 0x";
+ }
+ for (int i = 0; i < Math.min(length, DEBUG_LOG_NUM_BYTES); i++) {
+ ret += Byte.toHexString(mMessageBody[i], true /* upperCase */);
+
+ if ((i + 1) % 4 == 0) {
+ ret += " ";
+ }
+ }
+ if (length > DEBUG_LOG_NUM_BYTES) {
+ ret += "...";
+ }
+ ret += ")";
+
+ return ret;
+ }
}
diff --git a/android/hardware/location/NanoAppState.java b/android/hardware/location/NanoAppState.java
index 644031b..d05277d 100644
--- a/android/hardware/location/NanoAppState.java
+++ b/android/hardware/location/NanoAppState.java
@@ -15,6 +15,7 @@
*/
package android.hardware.location;
+import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -23,6 +24,7 @@
*
* @hide
*/
+@SystemApi
public final class NanoAppState implements Parcelable {
private long mNanoAppId;
private int mNanoAppVersion;
diff --git a/android/hardware/radio/Announcement.java b/android/hardware/radio/Announcement.java
new file mode 100644
index 0000000..166fe60
--- /dev/null
+++ b/android/hardware/radio/Announcement.java
@@ -0,0 +1,133 @@
+/**
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.radio;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @hide
+ */
+@SystemApi
+public final class Announcement implements Parcelable {
+
+ /** DAB alarm, RDS emergency program type (PTY 31). */
+ public static final int TYPE_EMERGENCY = 1;
+ /** DAB warning. */
+ public static final int TYPE_WARNING = 2;
+ /** DAB road traffic, RDS TA, HD Radio transportation. */
+ public static final int TYPE_TRAFFIC = 3;
+ /** Weather. */
+ public static final int TYPE_WEATHER = 4;
+ /** News. */
+ public static final int TYPE_NEWS = 5;
+ /** DAB event, special event. */
+ public static final int TYPE_EVENT = 6;
+ /** DAB sport report, RDS sports. */
+ public static final int TYPE_SPORT = 7;
+ /** All others. */
+ public static final int TYPE_MISC = 8;
+ /** @hide */
+ @IntDef(prefix = { "TYPE_" }, value = {
+ TYPE_EMERGENCY,
+ TYPE_WARNING,
+ TYPE_TRAFFIC,
+ TYPE_WEATHER,
+ TYPE_NEWS,
+ TYPE_EVENT,
+ TYPE_SPORT,
+ TYPE_MISC,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Type {}
+
+ /**
+ * Listener of announcement list events.
+ */
+ public interface OnListUpdatedListener {
+ /**
+ * An event called whenever a list of active announcements change.
+ *
+ * The entire list is sent each time a new announcement appears or any ends broadcasting.
+ *
+ * @param activeAnnouncements a full list of active announcements
+ */
+ void onListUpdated(Collection<Announcement> activeAnnouncements);
+ }
+
+ @NonNull private final ProgramSelector mSelector;
+ @Type private final int mType;
+ @NonNull private final Map<String, String> mVendorInfo;
+
+ /** @hide */
+ public Announcement(@NonNull ProgramSelector selector, @Type int type,
+ @NonNull Map<String, String> vendorInfo) {
+ mSelector = Objects.requireNonNull(selector);
+ mType = Objects.requireNonNull(type);
+ mVendorInfo = Objects.requireNonNull(vendorInfo);
+ }
+
+ private Announcement(@NonNull Parcel in) {
+ mSelector = in.readTypedObject(ProgramSelector.CREATOR);
+ mType = in.readInt();
+ mVendorInfo = Utils.readStringMap(in);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeTypedObject(mSelector, 0);
+ dest.writeInt(mType);
+ Utils.writeStringMap(dest, mVendorInfo);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<Announcement> CREATOR =
+ new Parcelable.Creator<Announcement>() {
+ public Announcement createFromParcel(Parcel in) {
+ return new Announcement(in);
+ }
+
+ public Announcement[] newArray(int size) {
+ return new Announcement[size];
+ }
+ };
+
+ public @NonNull ProgramSelector getSelector() {
+ return mSelector;
+ }
+
+ public @Type int getType() {
+ return mType;
+ }
+
+ public @NonNull Map<String, String> getVendorInfo() {
+ return mVendorInfo;
+ }
+}
diff --git a/android/hardware/radio/ProgramList.java b/android/hardware/radio/ProgramList.java
new file mode 100644
index 0000000..b2aa9ba
--- /dev/null
+++ b/android/hardware/radio/ProgramList.java
@@ -0,0 +1,427 @@
+/**
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.radio;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
+
+/**
+ * @hide
+ */
+@SystemApi
+public final class ProgramList implements AutoCloseable {
+
+ private final Object mLock = new Object();
+ private final Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> mPrograms =
+ new HashMap<>();
+
+ private final List<ListCallback> mListCallbacks = new ArrayList<>();
+ private final List<OnCompleteListener> mOnCompleteListeners = new ArrayList<>();
+ private OnCloseListener mOnCloseListener;
+ private boolean mIsClosed = false;
+ private boolean mIsComplete = false;
+
+ ProgramList() {}
+
+ /**
+ * Callback for list change operations.
+ */
+ public abstract static class ListCallback {
+ /**
+ * Called when item was modified or added to the list.
+ */
+ public void onItemChanged(@NonNull ProgramSelector.Identifier id) { }
+
+ /**
+ * Called when item was removed from the list.
+ */
+ public void onItemRemoved(@NonNull ProgramSelector.Identifier id) { }
+ }
+
+ /**
+ * Listener of list complete event.
+ */
+ public interface OnCompleteListener {
+ /**
+ * Called when the list turned complete (i.e. when the scan process
+ * came to an end).
+ */
+ void onComplete();
+ }
+
+ interface OnCloseListener {
+ void onClose();
+ }
+
+ /**
+ * Registers list change callback with executor.
+ */
+ public void registerListCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull ListCallback callback) {
+ registerListCallback(new ListCallback() {
+ public void onItemChanged(@NonNull ProgramSelector.Identifier id) {
+ executor.execute(() -> callback.onItemChanged(id));
+ }
+
+ public void onItemRemoved(@NonNull ProgramSelector.Identifier id) {
+ executor.execute(() -> callback.onItemRemoved(id));
+ }
+ });
+ }
+
+ /**
+ * Registers list change callback.
+ */
+ public void registerListCallback(@NonNull ListCallback callback) {
+ synchronized (mLock) {
+ if (mIsClosed) return;
+ mListCallbacks.add(Objects.requireNonNull(callback));
+ }
+ }
+
+ /**
+ * Unregisters list change callback.
+ */
+ public void unregisterListCallback(@NonNull ListCallback callback) {
+ synchronized (mLock) {
+ if (mIsClosed) return;
+ mListCallbacks.remove(Objects.requireNonNull(callback));
+ }
+ }
+
+ /**
+ * Adds list complete event listener with executor.
+ */
+ public void addOnCompleteListener(@NonNull @CallbackExecutor Executor executor,
+ @NonNull OnCompleteListener listener) {
+ addOnCompleteListener(() -> executor.execute(listener::onComplete));
+ }
+
+ /**
+ * Adds list complete event listener.
+ */
+ public void addOnCompleteListener(@NonNull OnCompleteListener listener) {
+ synchronized (mLock) {
+ if (mIsClosed) return;
+ mOnCompleteListeners.add(Objects.requireNonNull(listener));
+ if (mIsComplete) listener.onComplete();
+ }
+ }
+
+ /**
+ * Removes list complete event listener.
+ */
+ public void removeOnCompleteListener(@NonNull OnCompleteListener listener) {
+ synchronized (mLock) {
+ if (mIsClosed) return;
+ mOnCompleteListeners.remove(Objects.requireNonNull(listener));
+ }
+ }
+
+ void setOnCloseListener(@Nullable OnCloseListener listener) {
+ synchronized (mLock) {
+ if (mOnCloseListener != null) {
+ throw new IllegalStateException("Close callback is already set");
+ }
+ mOnCloseListener = listener;
+ }
+ }
+
+ /**
+ * Disables list updates and releases all resources.
+ */
+ public void close() {
+ synchronized (mLock) {
+ if (mIsClosed) return;
+ mIsClosed = true;
+ mPrograms.clear();
+ mListCallbacks.clear();
+ mOnCompleteListeners.clear();
+ if (mOnCloseListener != null) {
+ mOnCloseListener.onClose();
+ mOnCloseListener = null;
+ }
+ }
+ }
+
+ void apply(@NonNull Chunk chunk) {
+ synchronized (mLock) {
+ if (mIsClosed) return;
+
+ mIsComplete = false;
+
+ if (chunk.isPurge()) {
+ new HashSet<>(mPrograms.keySet()).stream().forEach(id -> removeLocked(id));
+ }
+
+ chunk.getRemoved().stream().forEach(id -> removeLocked(id));
+ chunk.getModified().stream().forEach(info -> putLocked(info));
+
+ if (chunk.isComplete()) {
+ mIsComplete = true;
+ mOnCompleteListeners.forEach(cb -> cb.onComplete());
+ }
+ }
+ }
+
+ private void putLocked(@NonNull RadioManager.ProgramInfo value) {
+ ProgramSelector.Identifier key = value.getSelector().getPrimaryId();
+ mPrograms.put(Objects.requireNonNull(key), value);
+ ProgramSelector.Identifier sel = value.getSelector().getPrimaryId();
+ mListCallbacks.forEach(cb -> cb.onItemChanged(sel));
+ }
+
+ private void removeLocked(@NonNull ProgramSelector.Identifier key) {
+ RadioManager.ProgramInfo removed = mPrograms.remove(Objects.requireNonNull(key));
+ if (removed == null) return;
+ ProgramSelector.Identifier sel = removed.getSelector().getPrimaryId();
+ mListCallbacks.forEach(cb -> cb.onItemRemoved(sel));
+ }
+
+ /**
+ * Converts the program list in its current shape to the static List<>.
+ *
+ * @return the new List<> object; it won't receive any further updates
+ */
+ public @NonNull List<RadioManager.ProgramInfo> toList() {
+ synchronized (mLock) {
+ return mPrograms.values().stream().collect(Collectors.toList());
+ }
+ }
+
+ /**
+ * Returns the program with a specified primary identifier.
+ *
+ * @param id primary identifier of a program to fetch
+ * @return the program info, or null if there is no such program on the list
+ */
+ public @Nullable RadioManager.ProgramInfo get(@NonNull ProgramSelector.Identifier id) {
+ synchronized (mLock) {
+ return mPrograms.get(Objects.requireNonNull(id));
+ }
+ }
+
+ /**
+ * Filter for the program list.
+ */
+ public static final class Filter implements Parcelable {
+ private final @NonNull Set<Integer> mIdentifierTypes;
+ private final @NonNull Set<ProgramSelector.Identifier> mIdentifiers;
+ private final boolean mIncludeCategories;
+ private final boolean mExcludeModifications;
+ private final @Nullable Map<String, String> mVendorFilter;
+
+ /**
+ * Constructor of program list filter.
+ *
+ * Arrays passed to this constructor become owned by this object, do not modify them later.
+ *
+ * @param identifierTypes see getIdentifierTypes()
+ * @param identifiers see getIdentifiers()
+ * @param includeCategories see areCategoriesIncluded()
+ * @param excludeModifications see areModificationsExcluded()
+ */
+ public Filter(@NonNull Set<Integer> identifierTypes,
+ @NonNull Set<ProgramSelector.Identifier> identifiers,
+ boolean includeCategories, boolean excludeModifications) {
+ mIdentifierTypes = Objects.requireNonNull(identifierTypes);
+ mIdentifiers = Objects.requireNonNull(identifiers);
+ mIncludeCategories = includeCategories;
+ mExcludeModifications = excludeModifications;
+ mVendorFilter = null;
+ }
+
+ /**
+ * @hide for framework use only
+ */
+ public Filter(@Nullable Map<String, String> vendorFilter) {
+ mIdentifierTypes = Collections.emptySet();
+ mIdentifiers = Collections.emptySet();
+ mIncludeCategories = false;
+ mExcludeModifications = false;
+ mVendorFilter = vendorFilter;
+ }
+
+ private Filter(@NonNull Parcel in) {
+ mIdentifierTypes = Utils.createIntSet(in);
+ mIdentifiers = Utils.createSet(in, ProgramSelector.Identifier.CREATOR);
+ mIncludeCategories = in.readByte() != 0;
+ mExcludeModifications = in.readByte() != 0;
+ mVendorFilter = Utils.readStringMap(in);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ Utils.writeIntSet(dest, mIdentifierTypes);
+ Utils.writeSet(dest, mIdentifiers);
+ dest.writeByte((byte) (mIncludeCategories ? 1 : 0));
+ dest.writeByte((byte) (mExcludeModifications ? 1 : 0));
+ Utils.writeStringMap(dest, mVendorFilter);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<Filter> CREATOR = new Parcelable.Creator<Filter>() {
+ public Filter createFromParcel(Parcel in) {
+ return new Filter(in);
+ }
+
+ public Filter[] newArray(int size) {
+ return new Filter[size];
+ }
+ };
+
+ /**
+ * @hide for framework use only
+ */
+ public Map<String, String> getVendorFilter() {
+ return mVendorFilter;
+ }
+
+ /**
+ * Returns the list of identifier types that satisfy the filter.
+ *
+ * If the program list entry contains at least one identifier of the type
+ * listed, it satisfies this condition.
+ *
+ * Empty list means no filtering on identifier type.
+ *
+ * @return the list of accepted identifier types, must not be modified
+ */
+ public @NonNull Set<Integer> getIdentifierTypes() {
+ return mIdentifierTypes;
+ }
+
+ /**
+ * Returns the list of identifiers that satisfy the filter.
+ *
+ * If the program list entry contains at least one listed identifier,
+ * it satisfies this condition.
+ *
+ * Empty list means no filtering on identifier.
+ *
+ * @return the list of accepted identifiers, must not be modified
+ */
+ public @NonNull Set<ProgramSelector.Identifier> getIdentifiers() {
+ return mIdentifiers;
+ }
+
+ /**
+ * Checks, if non-tunable entries that define tree structure on the
+ * program list (i.e. DAB ensembles) should be included.
+ */
+ public boolean areCategoriesIncluded() {
+ return mIncludeCategories;
+ }
+
+ /**
+ * Checks, if updates on entry modifications should be disabled.
+ *
+ * If true, 'modified' vector of ProgramListChunk must contain list
+ * additions only. Once the program is added to the list, it's not
+ * updated anymore.
+ */
+ public boolean areModificationsExcluded() {
+ return mExcludeModifications;
+ }
+ }
+
+ /**
+ * @hide This is a transport class used for internal communication between
+ * Broadcast Radio Service and RadioManager.
+ * Do not use it directly.
+ */
+ public static final class Chunk implements Parcelable {
+ private final boolean mPurge;
+ private final boolean mComplete;
+ private final @NonNull Set<RadioManager.ProgramInfo> mModified;
+ private final @NonNull Set<ProgramSelector.Identifier> mRemoved;
+
+ public Chunk(boolean purge, boolean complete,
+ @Nullable Set<RadioManager.ProgramInfo> modified,
+ @Nullable Set<ProgramSelector.Identifier> removed) {
+ mPurge = purge;
+ mComplete = complete;
+ mModified = (modified != null) ? modified : Collections.emptySet();
+ mRemoved = (removed != null) ? removed : Collections.emptySet();
+ }
+
+ private Chunk(@NonNull Parcel in) {
+ mPurge = in.readByte() != 0;
+ mComplete = in.readByte() != 0;
+ mModified = Utils.createSet(in, RadioManager.ProgramInfo.CREATOR);
+ mRemoved = Utils.createSet(in, ProgramSelector.Identifier.CREATOR);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByte((byte) (mPurge ? 1 : 0));
+ dest.writeByte((byte) (mComplete ? 1 : 0));
+ Utils.writeSet(dest, mModified);
+ Utils.writeSet(dest, mRemoved);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<Chunk> CREATOR = new Parcelable.Creator<Chunk>() {
+ public Chunk createFromParcel(Parcel in) {
+ return new Chunk(in);
+ }
+
+ public Chunk[] newArray(int size) {
+ return new Chunk[size];
+ }
+ };
+
+ public boolean isPurge() {
+ return mPurge;
+ }
+
+ public boolean isComplete() {
+ return mComplete;
+ }
+
+ public @NonNull Set<RadioManager.ProgramInfo> getModified() {
+ return mModified;
+ }
+
+ public @NonNull Set<ProgramSelector.Identifier> getRemoved() {
+ return mRemoved;
+ }
+ }
+}
diff --git a/android/hardware/radio/ProgramSelector.java b/android/hardware/radio/ProgramSelector.java
index 2211cee..0294a29 100644
--- a/android/hardware/radio/ProgramSelector.java
+++ b/android/hardware/radio/ProgramSelector.java
@@ -27,6 +27,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
@@ -59,24 +60,58 @@
*/
@SystemApi
public final class ProgramSelector implements Parcelable {
- /** Analogue AM radio (with or without RDS). */
+ /** Invalid program type.
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
+ public static final int PROGRAM_TYPE_INVALID = 0;
+ /** Analogue AM radio (with or without RDS).
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
public static final int PROGRAM_TYPE_AM = 1;
- /** analogue FM radio (with or without RDS). */
+ /** analogue FM radio (with or without RDS).
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
public static final int PROGRAM_TYPE_FM = 2;
- /** AM HD Radio. */
+ /** AM HD Radio.
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
public static final int PROGRAM_TYPE_AM_HD = 3;
- /** FM HD Radio. */
+ /** FM HD Radio.
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
public static final int PROGRAM_TYPE_FM_HD = 4;
- /** Digital audio broadcasting. */
+ /** Digital audio broadcasting.
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
public static final int PROGRAM_TYPE_DAB = 5;
- /** Digital Radio Mondiale. */
+ /** Digital Radio Mondiale.
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
public static final int PROGRAM_TYPE_DRMO = 6;
- /** SiriusXM Satellite Radio. */
+ /** SiriusXM Satellite Radio.
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
public static final int PROGRAM_TYPE_SXM = 7;
- /** Vendor-specific, not synced across devices. */
+ /** Vendor-specific, not synced across devices.
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
public static final int PROGRAM_TYPE_VENDOR_START = 1000;
+ /** @deprecated use {@link ProgramIdentifier} instead */
+ @Deprecated
public static final int PROGRAM_TYPE_VENDOR_END = 1999;
+ /** @deprecated use {@link ProgramIdentifier} instead */
+ @Deprecated
@IntDef(prefix = { "PROGRAM_TYPE_" }, value = {
+ PROGRAM_TYPE_INVALID,
PROGRAM_TYPE_AM,
PROGRAM_TYPE_FM,
PROGRAM_TYPE_AM_HD,
@@ -89,6 +124,7 @@
@Retention(RetentionPolicy.SOURCE)
public @interface ProgramType {}
+ public static final int IDENTIFIER_TYPE_INVALID = 0;
/** kHz */
public static final int IDENTIFIER_TYPE_AMFM_FREQUENCY = 1;
/** 16bit */
@@ -109,18 +145,46 @@
*
* The subchannel index is 0-based (where 0 is MPS and 1..7 are SPS),
* as opposed to HD Radio standard (where it's 1-based).
+ *
+ * @deprecated use IDENTIFIER_TYPE_HD_STATION_ID_EXT instead
*/
+ @Deprecated
public static final int IDENTIFIER_TYPE_HD_SUBCHANNEL = 4;
/**
- * 24bit compound primary identifier for DAB.
+ * 64bit additional identifier for HD Radio.
+ *
+ * Due to Station ID abuse, some HD_STATION_ID_EXT identifiers may be not
+ * globally unique. To provide a best-effort solution, a short version of
+ * station name may be carried as additional identifier and may be used
+ * by the tuner hardware to double-check tuning.
+ *
+ * The name is limited to the first 8 A-Z0-9 characters (lowercase letters
+ * must be converted to uppercase). Encoded in little-endian ASCII:
+ * the first character of the name is the LSB.
+ *
+ * For example: "Abc" is encoded as 0x434241.
+ */
+ public static final int IDENTIFIER_TYPE_HD_STATION_NAME = 10004;
+ /**
+ * @see {@link IDENTIFIER_TYPE_DAB_SID_EXT}
+ */
+ public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5;
+ /**
+ * 28bit compound primary identifier for Digital Audio Broadcasting.
*
* Consists of (from the LSB):
* - 16bit: SId;
- * - 8bit: ECC code.
+ * - 8bit: ECC code;
+ * - 4bit: SCIdS.
+ *
+ * SCIdS (Service Component Identifier within the Service) value
+ * of 0 represents the main service, while 1 and above represents
+ * secondary services.
+ *
* The remaining bits should be set to zeros when writing on the chip side
* and ignored when read.
*/
- public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5;
+ public static final int IDENTIFIER_TYPE_DAB_SID_EXT = IDENTIFIER_TYPE_DAB_SIDECC;
/** 16bit */
public static final int IDENTIFIER_TYPE_DAB_ENSEMBLE = 6;
/** 12bit */
@@ -131,7 +195,11 @@
public static final int IDENTIFIER_TYPE_DRMO_SERVICE_ID = 9;
/** kHz */
public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10;
- /** 1: AM, 2:FM */
+ /**
+ * 1: AM, 2:FM
+ * @deprecated use {@link IDENTIFIER_TYPE_DRMO_FREQUENCY} instead
+ */
+ @Deprecated
public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11;
/** 32bit */
public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12;
@@ -145,13 +213,29 @@
* type between VENDOR_START and VENDOR_END (eg. identifier type 1015 must
* not be used in any program type other than 1015).
*/
- public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_START = PROGRAM_TYPE_VENDOR_START;
- public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_END = PROGRAM_TYPE_VENDOR_END;
+ public static final int IDENTIFIER_TYPE_VENDOR_START = PROGRAM_TYPE_VENDOR_START;
+ /**
+ * @see {@link IDENTIFIER_TYPE_VENDOR_START}
+ */
+ public static final int IDENTIFIER_TYPE_VENDOR_END = PROGRAM_TYPE_VENDOR_END;
+ /**
+ * @deprecated use {@link IDENTIFIER_TYPE_VENDOR_START} instead
+ */
+ @Deprecated
+ public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_START = IDENTIFIER_TYPE_VENDOR_START;
+ /**
+ * @deprecated use {@link IDENTIFIER_TYPE_VENDOR_END} instead
+ */
+ @Deprecated
+ public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_END = IDENTIFIER_TYPE_VENDOR_END;
@IntDef(prefix = { "IDENTIFIER_TYPE_" }, value = {
+ IDENTIFIER_TYPE_INVALID,
IDENTIFIER_TYPE_AMFM_FREQUENCY,
IDENTIFIER_TYPE_RDS_PI,
IDENTIFIER_TYPE_HD_STATION_ID_EXT,
IDENTIFIER_TYPE_HD_SUBCHANNEL,
+ IDENTIFIER_TYPE_HD_STATION_NAME,
+ IDENTIFIER_TYPE_DAB_SID_EXT,
IDENTIFIER_TYPE_DAB_SIDECC,
IDENTIFIER_TYPE_DAB_ENSEMBLE,
IDENTIFIER_TYPE_DAB_SCID,
@@ -162,7 +246,7 @@
IDENTIFIER_TYPE_SXM_SERVICE_ID,
IDENTIFIER_TYPE_SXM_CHANNEL,
})
- @IntRange(from = IDENTIFIER_TYPE_VENDOR_PRIMARY_START, to = IDENTIFIER_TYPE_VENDOR_PRIMARY_END)
+ @IntRange(from = IDENTIFIER_TYPE_VENDOR_START, to = IDENTIFIER_TYPE_VENDOR_END)
@Retention(RetentionPolicy.SOURCE)
public @interface IdentifierType {}
@@ -201,7 +285,9 @@
* Type of a radio technology.
*
* @return program type.
+ * @deprecated use {@link getPrimaryId} instead
*/
+ @Deprecated
public @ProgramType int getProgramType() {
return mProgramType;
}
@@ -268,13 +354,48 @@
* Vendor identifiers are passed as-is to the HAL implementation,
* preserving elements order.
*
- * @return a array of vendor identifiers, must not be modified.
+ * @return an array of vendor identifiers, must not be modified.
+ * @deprecated for HAL 1.x compatibility;
+ * HAL 2.x uses standard primary/secondary lists for vendor IDs
*/
+ @Deprecated
public @NonNull long[] getVendorIds() {
return mVendorIds;
}
/**
+ * Creates an equivalent ProgramSelector with a given secondary identifier preferred.
+ *
+ * Used to point to a specific physical identifier for technologies that may broadcast the same
+ * program on different channels. For example, with a DAB program broadcasted over multiple
+ * ensembles, the radio hardware may select the one with the strongest signal. The UI may select
+ * preferred ensemble though, so the radio hardware may try to use it in the first place.
+ *
+ * This is a best-effort hint for the tuner, not a guaranteed behavior.
+ *
+ * Setting the given secondary identifier as preferred means filtering out other secondary
+ * identifiers of its type and adding it to the list.
+ *
+ * @param preferred preferred secondary identifier
+ * @return a new ProgramSelector with a given secondary identifier preferred
+ */
+ public @NonNull ProgramSelector withSecondaryPreferred(@NonNull Identifier preferred) {
+ int preferredType = preferred.getType();
+ Identifier[] secondaryIds = Stream.concat(
+ // remove other identifiers of that type
+ Arrays.stream(mSecondaryIds).filter(id -> id.getType() != preferredType),
+ // add preferred identifier instead
+ Stream.of(preferred)).toArray(Identifier[]::new);
+
+ return new ProgramSelector(
+ mProgramType,
+ mPrimaryId,
+ secondaryIds,
+ mVendorIds
+ );
+ }
+
+ /**
* Builds new ProgramSelector for AM/FM frequency.
*
* @param band the band.
@@ -423,6 +544,10 @@
private final long mValue;
public Identifier(@IdentifierType int type, long value) {
+ if (type == IDENTIFIER_TYPE_HD_STATION_NAME) {
+ // see getType
+ type = IDENTIFIER_TYPE_HD_SUBCHANNEL;
+ }
mType = type;
mValue = value;
}
@@ -433,6 +558,13 @@
* @return type of an identifier.
*/
public @IdentifierType int getType() {
+ if (mType == IDENTIFIER_TYPE_HD_SUBCHANNEL && mValue > 10) {
+ /* HD_SUBCHANNEL and HD_STATION_NAME use the same identifier type, but they differ
+ * in possible values: sub channel is 0-7, station name is greater than ASCII space
+ * code (32).
+ */
+ return IDENTIFIER_TYPE_HD_STATION_NAME;
+ }
return mType;
}
diff --git a/android/hardware/radio/RadioManager.java b/android/hardware/radio/RadioManager.java
index 4d54e31..b00f603 100644
--- a/android/hardware/radio/RadioManager.java
+++ b/android/hardware/radio/RadioManager.java
@@ -17,8 +17,10 @@
package android.hardware.radio;
import android.Manifest;
+import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
@@ -32,13 +34,19 @@
import android.text.TextUtils;
import android.util.Log;
+import com.android.internal.util.Preconditions;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.Executor;
import java.util.stream.Collectors;
/**
@@ -119,24 +127,70 @@
* @see BandDescriptor */
public static final int REGION_KOREA = 4;
- private static void writeStringMap(@NonNull Parcel dest, @NonNull Map<String, String> map) {
- dest.writeInt(map.size());
- for (Map.Entry<String, String> entry : map.entrySet()) {
- dest.writeString(entry.getKey());
- dest.writeString(entry.getValue());
- }
- }
+ /**
+ * Forces mono audio stream reception.
+ *
+ * Analog broadcasts can recover poor reception conditions by jointing
+ * stereo channels into one. Mainly for, but not limited to AM/FM.
+ */
+ public static final int CONFIG_FORCE_MONO = 1;
+ /**
+ * Forces the analog playback for the supporting radio technology.
+ *
+ * User may disable digital playback for FM HD Radio or hybrid FM/DAB with
+ * this option. This is purely user choice, ie. does not reflect digital-
+ * analog handover state managed from the HAL implementation side.
+ *
+ * Some radio technologies may not support this, ie. DAB.
+ */
+ public static final int CONFIG_FORCE_ANALOG = 2;
+ /**
+ * Forces the digital playback for the supporting radio technology.
+ *
+ * User may disable digital-analog handover that happens with poor
+ * reception conditions. With digital forced, the radio will remain silent
+ * instead of switching to analog channel if it's available. This is purely
+ * user choice, it does not reflect the actual state of handover.
+ */
+ public static final int CONFIG_FORCE_DIGITAL = 3;
+ /**
+ * RDS Alternative Frequencies.
+ *
+ * If set and the currently tuned RDS station broadcasts on multiple
+ * channels, radio tuner automatically switches to the best available
+ * alternative.
+ */
+ public static final int CONFIG_RDS_AF = 4;
+ /**
+ * RDS region-specific program lock-down.
+ *
+ * Allows user to lock to the current region as they move into the
+ * other region.
+ */
+ public static final int CONFIG_RDS_REG = 5;
+ /** Enables DAB-DAB hard- and implicit-linking (the same content). */
+ public static final int CONFIG_DAB_DAB_LINKING = 6;
+ /** Enables DAB-FM hard- and implicit-linking (the same content). */
+ public static final int CONFIG_DAB_FM_LINKING = 7;
+ /** Enables DAB-DAB soft-linking (related content). */
+ public static final int CONFIG_DAB_DAB_SOFT_LINKING = 8;
+ /** Enables DAB-FM soft-linking (related content). */
+ public static final int CONFIG_DAB_FM_SOFT_LINKING = 9;
- private static @NonNull Map<String, String> readStringMap(@NonNull Parcel in) {
- int size = in.readInt();
- Map<String, String> map = new HashMap<>();
- while (size-- > 0) {
- String key = in.readString();
- String value = in.readString();
- map.put(key, value);
- }
- return map;
- }
+ /** @hide */
+ @IntDef(prefix = { "CONFIG_" }, value = {
+ CONFIG_FORCE_MONO,
+ CONFIG_FORCE_ANALOG,
+ CONFIG_FORCE_DIGITAL,
+ CONFIG_RDS_AF,
+ CONFIG_RDS_REG,
+ CONFIG_DAB_DAB_LINKING,
+ CONFIG_DAB_FM_LINKING,
+ CONFIG_DAB_DAB_SOFT_LINKING,
+ CONFIG_DAB_FM_SOFT_LINKING,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ConfigFlag {}
/*****************************************************************************
* Lists properties, options and radio bands supported by a given broadcast radio module.
@@ -349,7 +403,7 @@
mIsBgScanSupported = in.readInt() == 1;
mSupportedProgramTypes = arrayToSet(in.createIntArray());
mSupportedIdentifierTypes = arrayToSet(in.createIntArray());
- mVendorInfo = readStringMap(in);
+ mVendorInfo = Utils.readStringMap(in);
}
public static final Parcelable.Creator<ModuleProperties> CREATOR
@@ -379,7 +433,7 @@
dest.writeInt(mIsBgScanSupported ? 1 : 0);
dest.writeIntArray(setToArray(mSupportedProgramTypes));
dest.writeIntArray(setToArray(mSupportedIdentifierTypes));
- writeStringMap(dest, mVendorInfo);
+ Utils.writeStringMap(dest, mVendorInfo);
}
@Override
@@ -645,7 +699,8 @@
private final boolean mAf;
private final boolean mEa;
- FmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing,
+ /** @hide */
+ public FmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing,
boolean stereo, boolean rds, boolean ta, boolean af, boolean ea) {
super(region, type, lowerLimit, upperLimit, spacing);
mStereo = stereo;
@@ -771,7 +826,8 @@
private final boolean mStereo;
- AmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing,
+ /** @hide */
+ public AmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing,
boolean stereo) {
super(region, type, lowerLimit, upperLimit, spacing);
mStereo = stereo;
@@ -843,10 +899,10 @@
/** Radio band configuration. */
public static class BandConfig implements Parcelable {
- final BandDescriptor mDescriptor;
+ @NonNull final BandDescriptor mDescriptor;
BandConfig(BandDescriptor descriptor) {
- mDescriptor = descriptor;
+ mDescriptor = Objects.requireNonNull(descriptor);
}
BandConfig(int region, int type, int lowerLimit, int upperLimit, int spacing) {
@@ -968,7 +1024,8 @@
private final boolean mAf;
private final boolean mEa;
- FmBandConfig(FmBandDescriptor descriptor) {
+ /** @hide */
+ public FmBandConfig(FmBandDescriptor descriptor) {
super((BandDescriptor)descriptor);
mStereo = descriptor.isStereoSupported();
mRds = descriptor.isRdsSupported();
@@ -1204,7 +1261,8 @@
public static class AmBandConfig extends BandConfig {
private final boolean mStereo;
- AmBandConfig(AmBandDescriptor descriptor) {
+ /** @hide */
+ public AmBandConfig(AmBandDescriptor descriptor) {
super((BandDescriptor)descriptor);
mStereo = descriptor.isStereoSupported();
}
@@ -1329,34 +1387,44 @@
};
}
- /** Radio program information returned by
- * {@link RadioTuner#getProgramInformation(RadioManager.ProgramInfo[])} */
+ /** Radio program information. */
public static class ProgramInfo implements Parcelable {
- // sourced from hardware/interfaces/broadcastradio/1.1/types.hal
+ // sourced from hardware/interfaces/broadcastradio/2.0/types.hal
private static final int FLAG_LIVE = 1 << 0;
private static final int FLAG_MUTED = 1 << 1;
private static final int FLAG_TRAFFIC_PROGRAM = 1 << 2;
private static final int FLAG_TRAFFIC_ANNOUNCEMENT = 1 << 3;
+ private static final int FLAG_TUNED = 1 << 4;
+ private static final int FLAG_STEREO = 1 << 5;
@NonNull private final ProgramSelector mSelector;
- private final boolean mTuned;
- private final boolean mStereo;
- private final boolean mDigital;
- private final int mFlags;
- private final int mSignalStrength;
- private final RadioMetadata mMetadata;
+ @Nullable private final ProgramSelector.Identifier mLogicallyTunedTo;
+ @Nullable private final ProgramSelector.Identifier mPhysicallyTunedTo;
+ @NonNull private final Collection<ProgramSelector.Identifier> mRelatedContent;
+ private final int mInfoFlags;
+ private final int mSignalQuality;
+ @Nullable private final RadioMetadata mMetadata;
@NonNull private final Map<String, String> mVendorInfo;
- ProgramInfo(@NonNull ProgramSelector selector, boolean tuned, boolean stereo,
- boolean digital, int signalStrength, RadioMetadata metadata, int flags,
- Map<String, String> vendorInfo) {
- mSelector = selector;
- mTuned = tuned;
- mStereo = stereo;
- mDigital = digital;
- mFlags = flags;
- mSignalStrength = signalStrength;
+ /** @hide */
+ public ProgramInfo(@NonNull ProgramSelector selector,
+ @Nullable ProgramSelector.Identifier logicallyTunedTo,
+ @Nullable ProgramSelector.Identifier physicallyTunedTo,
+ @Nullable Collection<ProgramSelector.Identifier> relatedContent,
+ int infoFlags, int signalQuality, @Nullable RadioMetadata metadata,
+ @Nullable Map<String, String> vendorInfo) {
+ mSelector = Objects.requireNonNull(selector);
+ mLogicallyTunedTo = logicallyTunedTo;
+ mPhysicallyTunedTo = physicallyTunedTo;
+ if (relatedContent == null) {
+ mRelatedContent = Collections.emptyList();
+ } else {
+ Preconditions.checkCollectionElementsNotNull(relatedContent, "relatedContent");
+ mRelatedContent = relatedContent;
+ }
+ mInfoFlags = infoFlags;
+ mSignalQuality = signalQuality;
mMetadata = metadata;
mVendorInfo = (vendorInfo == null) ? new HashMap<>() : vendorInfo;
}
@@ -1370,6 +1438,51 @@
return mSelector;
}
+ /**
+ * Identifier currently used for program selection.
+ *
+ * This identifier can be used to determine which technology is
+ * currently being used for reception.
+ *
+ * Some program selectors contain tuning information for different radio
+ * technologies (i.e. FM RDS and DAB). For example, user may tune using
+ * a ProgramSelector with RDS_PI primary identifier, but the tuner hardware
+ * may choose to use DAB technology to make actual tuning. This identifier
+ * must reflect that.
+ */
+ public @Nullable ProgramSelector.Identifier getLogicallyTunedTo() {
+ return mLogicallyTunedTo;
+ }
+
+ /**
+ * Identifier currently used by hardware to physically tune to a channel.
+ *
+ * Some radio technologies broadcast the same program on multiple channels,
+ * i.e. with RDS AF the same program may be broadcasted on multiple
+ * alternative frequencies; the same DAB program may be broadcast on
+ * multiple ensembles. This identifier points to the channel to which the
+ * radio hardware is physically tuned to.
+ */
+ public @Nullable ProgramSelector.Identifier getPhysicallyTunedTo() {
+ return mPhysicallyTunedTo;
+ }
+
+ /**
+ * Primary identifiers of related contents.
+ *
+ * Some radio technologies provide pointers to other programs that carry
+ * related content (i.e. DAB soft-links). This field is a list of pointers
+ * to other programs on the program list.
+ *
+ * Please note, that these identifiers does not have to exist on the program
+ * list - i.e. DAB tuner may provide information on FM RDS alternatives
+ * despite not supporting FM RDS. If the system has multiple tuners, another
+ * one may have it on its list.
+ */
+ public @Nullable Collection<ProgramSelector.Identifier> getRelatedContent() {
+ return mRelatedContent;
+ }
+
/** Main channel expressed in units according to band type.
* Currently all defined band types express channels as frequency in kHz
* @return the program channel
@@ -1404,19 +1517,28 @@
* @return {@code true} if currently tuned, {@code false} otherwise.
*/
public boolean isTuned() {
- return mTuned;
+ return (mInfoFlags & FLAG_TUNED) != 0;
}
+
/** {@code true} if the received program is stereo
* @return {@code true} if stereo, {@code false} otherwise.
*/
public boolean isStereo() {
- return mStereo;
+ return (mInfoFlags & FLAG_STEREO) != 0;
}
+
/** {@code true} if the received program is digital (e.g HD radio)
* @return {@code true} if digital, {@code false} otherwise.
+ * @deprecated Use {@link getLogicallyTunedTo()} instead.
*/
+ @Deprecated
public boolean isDigital() {
- return mDigital;
+ ProgramSelector.Identifier id = mLogicallyTunedTo;
+ if (id == null) id = mSelector.getPrimaryId();
+
+ int type = id.getType();
+ return (type != ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY
+ && type != ProgramSelector.IDENTIFIER_TYPE_RDS_PI);
}
/**
@@ -1425,7 +1547,7 @@
* usually targetted at reduced latency.
*/
public boolean isLive() {
- return (mFlags & FLAG_LIVE) != 0;
+ return (mInfoFlags & FLAG_LIVE) != 0;
}
/**
@@ -1435,7 +1557,7 @@
* It does NOT mean the user has muted audio.
*/
public boolean isMuted() {
- return (mFlags & FLAG_MUTED) != 0;
+ return (mInfoFlags & FLAG_MUTED) != 0;
}
/**
@@ -1443,7 +1565,7 @@
* regularily.
*/
public boolean isTrafficProgram() {
- return (mFlags & FLAG_TRAFFIC_PROGRAM) != 0;
+ return (mInfoFlags & FLAG_TRAFFIC_PROGRAM) != 0;
}
/**
@@ -1451,15 +1573,18 @@
* at the very moment.
*/
public boolean isTrafficAnnouncementActive() {
- return (mFlags & FLAG_TRAFFIC_ANNOUNCEMENT) != 0;
+ return (mInfoFlags & FLAG_TRAFFIC_ANNOUNCEMENT) != 0;
}
- /** Signal strength indicator from 0 (no signal) to 100 (excellent)
- * @return the signal strength indication.
+ /**
+ * Signal quality (as opposed to the name) indication from 0 (no signal)
+ * to 100 (excellent)
+ * @return the signal quality indication.
*/
public int getSignalStrength() {
- return mSignalStrength;
+ return mSignalQuality;
}
+
/** Metadata currently received from this station.
* null if no metadata have been received
* @return current meta data received from this program.
@@ -1483,18 +1608,14 @@
}
private ProgramInfo(Parcel in) {
- mSelector = in.readParcelable(null);
- mTuned = in.readByte() == 1;
- mStereo = in.readByte() == 1;
- mDigital = in.readByte() == 1;
- mSignalStrength = in.readInt();
- if (in.readByte() == 1) {
- mMetadata = RadioMetadata.CREATOR.createFromParcel(in);
- } else {
- mMetadata = null;
- }
- mFlags = in.readInt();
- mVendorInfo = readStringMap(in);
+ mSelector = Objects.requireNonNull(in.readTypedObject(ProgramSelector.CREATOR));
+ mLogicallyTunedTo = in.readTypedObject(ProgramSelector.Identifier.CREATOR);
+ mPhysicallyTunedTo = in.readTypedObject(ProgramSelector.Identifier.CREATOR);
+ mRelatedContent = in.createTypedArrayList(ProgramSelector.Identifier.CREATOR);
+ mInfoFlags = in.readInt();
+ mSignalQuality = in.readInt();
+ mMetadata = in.readTypedObject(RadioMetadata.CREATOR);
+ mVendorInfo = Utils.readStringMap(in);
}
public static final Parcelable.Creator<ProgramInfo> CREATOR
@@ -1510,19 +1631,14 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeParcelable(mSelector, 0);
- dest.writeByte((byte)(mTuned ? 1 : 0));
- dest.writeByte((byte)(mStereo ? 1 : 0));
- dest.writeByte((byte)(mDigital ? 1 : 0));
- dest.writeInt(mSignalStrength);
- if (mMetadata == null) {
- dest.writeByte((byte)0);
- } else {
- dest.writeByte((byte)1);
- mMetadata.writeToParcel(dest, flags);
- }
- dest.writeInt(mFlags);
- writeStringMap(dest, mVendorInfo);
+ dest.writeTypedObject(mSelector, flags);
+ dest.writeTypedObject(mLogicallyTunedTo, flags);
+ dest.writeTypedObject(mPhysicallyTunedTo, flags);
+ Utils.writeTypedCollection(dest, mRelatedContent);
+ dest.writeInt(mInfoFlags);
+ dest.writeInt(mSignalQuality);
+ dest.writeTypedObject(mMetadata, flags);
+ Utils.writeStringMap(dest, mVendorInfo);
}
@Override
@@ -1532,52 +1648,38 @@
@Override
public String toString() {
- return "ProgramInfo [mSelector=" + mSelector
- + ", mTuned=" + mTuned + ", mStereo=" + mStereo + ", mDigital=" + mDigital
- + ", mFlags=" + mFlags + ", mSignalStrength=" + mSignalStrength
- + ((mMetadata == null) ? "" : (", mMetadata=" + mMetadata.toString()))
+ return "ProgramInfo"
+ + " [selector=" + mSelector
+ + ", logicallyTunedTo=" + Objects.toString(mLogicallyTunedTo)
+ + ", physicallyTunedTo=" + Objects.toString(mPhysicallyTunedTo)
+ + ", relatedContent=" + mRelatedContent.size()
+ + ", infoFlags=" + mInfoFlags
+ + ", mSignalQuality=" + mSignalQuality
+ + ", mMetadata=" + Objects.toString(mMetadata)
+ "]";
}
@Override
public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + mSelector.hashCode();
- result = prime * result + (mTuned ? 1 : 0);
- result = prime * result + (mStereo ? 1 : 0);
- result = prime * result + (mDigital ? 1 : 0);
- result = prime * result + mFlags;
- result = prime * result + mSignalStrength;
- result = prime * result + ((mMetadata == null) ? 0 : mMetadata.hashCode());
- result = prime * result + mVendorInfo.hashCode();
- return result;
+ return Objects.hash(mSelector, mLogicallyTunedTo, mPhysicallyTunedTo,
+ mRelatedContent, mInfoFlags, mSignalQuality, mMetadata, mVendorInfo);
}
@Override
public boolean equals(Object obj) {
- if (this == obj)
- return true;
- if (!(obj instanceof ProgramInfo))
- return false;
+ if (this == obj) return true;
+ if (!(obj instanceof ProgramInfo)) return false;
ProgramInfo other = (ProgramInfo) obj;
- if (!mSelector.equals(other.getSelector())) return false;
- if (mTuned != other.isTuned())
- return false;
- if (mStereo != other.isStereo())
- return false;
- if (mDigital != other.isDigital())
- return false;
- if (mFlags != other.mFlags)
- return false;
- if (mSignalStrength != other.getSignalStrength())
- return false;
- if (mMetadata == null) {
- if (other.getMetadata() != null)
- return false;
- } else if (!mMetadata.equals(other.getMetadata()))
- return false;
- if (!mVendorInfo.equals(other.mVendorInfo)) return false;
+
+ if (!Objects.equals(mSelector, other.mSelector)) return false;
+ if (!Objects.equals(mLogicallyTunedTo, other.mLogicallyTunedTo)) return false;
+ if (!Objects.equals(mPhysicallyTunedTo, other.mPhysicallyTunedTo)) return false;
+ if (!Objects.equals(mRelatedContent, other.mRelatedContent)) return false;
+ if (mInfoFlags != other.mInfoFlags) return false;
+ if (mSignalQuality != other.mSignalQuality) return false;
+ if (!Objects.equals(mMetadata, other.mMetadata)) return false;
+ if (!Objects.equals(mVendorInfo, other.mVendorInfo)) return false;
+
return true;
}
}
@@ -1649,15 +1751,78 @@
TunerCallbackAdapter halCallback = new TunerCallbackAdapter(callback, handler);
try {
tuner = mService.openTuner(moduleId, config, withAudio, halCallback);
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to open tuner", e);
+ } catch (RemoteException | IllegalArgumentException ex) {
+ Log.e(TAG, "Failed to open tuner", ex);
return null;
}
if (tuner == null) {
Log.e(TAG, "Failed to open tuner");
return null;
}
- return new TunerAdapter(tuner, config != null ? config.getType() : BAND_INVALID);
+ return new TunerAdapter(tuner, halCallback,
+ config != null ? config.getType() : BAND_INVALID);
+ }
+
+ private final Map<Announcement.OnListUpdatedListener, ICloseHandle> mAnnouncementListeners =
+ new HashMap<>();
+
+ /**
+ * Adds new announcement listener.
+ *
+ * @param enabledAnnouncementTypes a set of announcement types to listen to
+ * @param listener announcement listener
+ */
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+ public void addAnnouncementListener(@NonNull Set<Integer> enabledAnnouncementTypes,
+ @NonNull Announcement.OnListUpdatedListener listener) {
+ addAnnouncementListener(cmd -> cmd.run(), enabledAnnouncementTypes, listener);
+ }
+
+ /**
+ * Adds new announcement listener with executor.
+ *
+ * @param executor the executor
+ * @param enabledAnnouncementTypes a set of announcement types to listen to
+ * @param listener announcement listener
+ */
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+ public void addAnnouncementListener(@NonNull @CallbackExecutor Executor executor,
+ @NonNull Set<Integer> enabledAnnouncementTypes,
+ @NonNull Announcement.OnListUpdatedListener listener) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(listener);
+ int[] types = enabledAnnouncementTypes.stream().mapToInt(Integer::intValue).toArray();
+ IAnnouncementListener listenerIface = new IAnnouncementListener.Stub() {
+ public void onListUpdated(List<Announcement> activeAnnouncements) {
+ executor.execute(() -> listener.onListUpdated(activeAnnouncements));
+ }
+ };
+ synchronized (mAnnouncementListeners) {
+ ICloseHandle closeHandle = null;
+ try {
+ closeHandle = mService.addAnnouncementListener(types, listenerIface);
+ } catch (RemoteException ex) {
+ ex.rethrowFromSystemServer();
+ }
+ Objects.requireNonNull(closeHandle);
+ ICloseHandle oldCloseHandle = mAnnouncementListeners.put(listener, closeHandle);
+ if (oldCloseHandle != null) Utils.close(oldCloseHandle);
+ }
+ }
+
+ /**
+ * Removes previously registered announcement listener.
+ *
+ * @param listener announcement listener, previously registered with
+ * {@link addAnnouncementListener}
+ */
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+ public void removeAnnouncementListener(@NonNull Announcement.OnListUpdatedListener listener) {
+ Objects.requireNonNull(listener);
+ synchronized (mAnnouncementListeners) {
+ ICloseHandle closeHandle = mAnnouncementListeners.remove(listener);
+ if (closeHandle != null) Utils.close(closeHandle);
+ }
}
@NonNull private final Context mContext;
diff --git a/android/hardware/radio/RadioTuner.java b/android/hardware/radio/RadioTuner.java
index e93fd5f..ed20c4a 100644
--- a/android/hardware/radio/RadioTuner.java
+++ b/android/hardware/radio/RadioTuner.java
@@ -280,17 +280,37 @@
* @throws IllegalStateException if the scan is in progress or has not been started,
* startBackgroundScan() call may fix it.
* @throws IllegalArgumentException if the vendorFilter argument is not valid.
+ * @deprecated Use {@link getDynamicProgramList} instead.
*/
+ @Deprecated
public abstract @NonNull List<RadioManager.ProgramInfo>
getProgramList(@Nullable Map<String, String> vendorFilter);
/**
+ * Get the dynamic list of discovered radio stations.
+ *
+ * The list object is updated asynchronously; to get the updates register
+ * with {@link ProgramList#addListCallback}.
+ *
+ * When the returned object is no longer used, it must be closed.
+ *
+ * @param filter filter for the list, or null to get the full list.
+ * @return the dynamic program list object, close it after use
+ * or {@code null} if program list is not supported by the tuner
+ */
+ public @Nullable ProgramList getDynamicProgramList(@Nullable ProgramList.Filter filter) {
+ return null;
+ }
+
+ /**
* Checks, if the analog playback is forced, see setAnalogForced.
*
* @throws IllegalStateException if the switch is not supported at current
* configuration.
* @return {@code true} if analog is forced, {@code false} otherwise.
+ * @deprecated Use {@link isConfigFlagSet(int)} instead.
*/
+ @Deprecated
public abstract boolean isAnalogForced();
/**
@@ -305,10 +325,50 @@
* @param isForced {@code true} to force analog, {@code false} for a default behaviour.
* @throws IllegalStateException if the switch is not supported at current
* configuration.
+ * @deprecated Use {@link setConfigFlag(int, boolean)} instead.
*/
+ @Deprecated
public abstract void setAnalogForced(boolean isForced);
/**
+ * Checks, if a given config flag is supported
+ *
+ * @param flag Flag to check.
+ * @return True, if the flag is supported.
+ */
+ public boolean isConfigFlagSupported(@RadioManager.ConfigFlag int flag) {
+ return false;
+ }
+
+ /**
+ * Fetches the current setting of a given config flag.
+ *
+ * The success/failure result is consistent with isConfigFlagSupported.
+ *
+ * @param flag Flag to fetch.
+ * @return The current value of the flag.
+ * @throws IllegalStateException if the flag is not applicable right now.
+ * @throws UnsupportedOperationException if the flag is not supported at all.
+ */
+ public boolean isConfigFlagSet(@RadioManager.ConfigFlag int flag) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Sets the config flag.
+ *
+ * The success/failure result is consistent with isConfigFlagSupported.
+ *
+ * @param flag Flag to set.
+ * @param value The new value of a given flag.
+ * @throws IllegalStateException if the flag is not applicable right now.
+ * @throws UnsupportedOperationException if the flag is not supported at all.
+ */
+ public void setConfigFlag(@RadioManager.ConfigFlag int flag, boolean value) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
* Generic method for setting vendor-specific parameter values.
* The framework does not interpret the parameters, they are passed
* in an opaque manner between a vendor application and HAL.
@@ -316,6 +376,7 @@
* Framework does not make any assumptions on the keys or values, other than
* ones stated in VendorKeyValue documentation (a requirement of key
* prefixes).
+ * See VendorKeyValue at hardware/interfaces/broadcastradio/2.0/types.hal.
*
* For each pair in the result map, the key will be one of the keys
* contained in the input (possibly with wildcards expanded), and the value
@@ -332,10 +393,11 @@
*
* @param parameters Vendor-specific key-value pairs.
* @return Operation completion status for parameters being set.
- * @hide FutureFeature
*/
- public abstract @NonNull Map<String, String>
- setParameters(@NonNull Map<String, String> parameters);
+ public @NonNull Map<String, String>
+ setParameters(@NonNull Map<String, String> parameters) {
+ throw new UnsupportedOperationException();
+ }
/**
* Generic method for retrieving vendor-specific parameter values.
@@ -355,10 +417,11 @@
*
* @param keys Parameter keys to fetch.
* @return Vendor-specific key-value pairs.
- * @hide FutureFeature
*/
- public abstract @NonNull Map<String, String>
- getParameters(@NonNull List<String> keys);
+ public @NonNull Map<String, String>
+ getParameters(@NonNull List<String> keys) {
+ throw new UnsupportedOperationException();
+ }
/**
* Get current antenna connection state for current configuration.
@@ -494,7 +557,6 @@
* asynchronously.
*
* @param parameters Vendor-specific key-value pairs.
- * @hide FutureFeature
*/
public void onParametersUpdated(@NonNull Map<String, String> parameters) {}
}
diff --git a/android/hardware/radio/TunerAdapter.java b/android/hardware/radio/TunerAdapter.java
index 864d17c..91944bf 100644
--- a/android/hardware/radio/TunerAdapter.java
+++ b/android/hardware/radio/TunerAdapter.java
@@ -33,15 +33,18 @@
private static final String TAG = "BroadcastRadio.TunerAdapter";
@NonNull private final ITuner mTuner;
+ @NonNull private final TunerCallbackAdapter mCallback;
private boolean mIsClosed = false;
private @RadioManager.Band int mBand;
- TunerAdapter(ITuner tuner, @RadioManager.Band int band) {
- if (tuner == null) {
- throw new NullPointerException();
- }
- mTuner = tuner;
+ private ProgramList mLegacyListProxy;
+ private Map<String, String> mLegacyListFilter;
+
+ TunerAdapter(@NonNull ITuner tuner, @NonNull TunerCallbackAdapter callback,
+ @RadioManager.Band int band) {
+ mTuner = Objects.requireNonNull(tuner);
+ mCallback = Objects.requireNonNull(callback);
mBand = band;
}
@@ -53,6 +56,10 @@
return;
}
mIsClosed = true;
+ if (mLegacyListProxy != null) {
+ mLegacyListProxy.close();
+ mLegacyListProxy = null;
+ }
}
try {
mTuner.close();
@@ -63,6 +70,7 @@
@Override
public int setConfiguration(RadioManager.BandConfig config) {
+ if (config == null) return RadioManager.STATUS_BAD_VALUE;
try {
mTuner.setConfiguration(config);
mBand = config.getType();
@@ -226,26 +234,90 @@
@Override
public @NonNull List<RadioManager.ProgramInfo>
getProgramList(@Nullable Map<String, String> vendorFilter) {
- try {
- return mTuner.getProgramList(vendorFilter);
- } catch (RemoteException e) {
- throw new RuntimeException("service died", e);
+ synchronized (mTuner) {
+ if (mLegacyListProxy == null || !Objects.equals(mLegacyListFilter, vendorFilter)) {
+ Log.i(TAG, "Program list filter has changed, requesting new list");
+ mLegacyListProxy = new ProgramList();
+ mLegacyListFilter = vendorFilter;
+
+ mCallback.clearLastCompleteList();
+ mCallback.setProgramListObserver(mLegacyListProxy, () -> { });
+ try {
+ mTuner.startProgramListUpdates(new ProgramList.Filter(vendorFilter));
+ } catch (RemoteException ex) {
+ throw new RuntimeException("service died", ex);
+ }
+ }
+
+ List<RadioManager.ProgramInfo> list = mCallback.getLastCompleteList();
+ if (list == null) throw new IllegalStateException("Program list is not ready yet");
+ return list;
+ }
+ }
+
+ @Override
+ public @Nullable ProgramList getDynamicProgramList(@Nullable ProgramList.Filter filter) {
+ synchronized (mTuner) {
+ if (mLegacyListProxy != null) {
+ mLegacyListProxy.close();
+ mLegacyListProxy = null;
+ }
+ mLegacyListFilter = null;
+
+ ProgramList list = new ProgramList();
+ mCallback.setProgramListObserver(list, () -> {
+ try {
+ mTuner.stopProgramListUpdates();
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Couldn't stop program list updates", ex);
+ }
+ });
+
+ try {
+ mTuner.startProgramListUpdates(filter);
+ } catch (UnsupportedOperationException ex) {
+ return null;
+ } catch (RemoteException ex) {
+ mCallback.setProgramListObserver(null, () -> { });
+ throw new RuntimeException("service died", ex);
+ }
+
+ return list;
}
}
@Override
public boolean isAnalogForced() {
+ return isConfigFlagSet(RadioManager.CONFIG_FORCE_ANALOG);
+ }
+
+ @Override
+ public void setAnalogForced(boolean isForced) {
+ setConfigFlag(RadioManager.CONFIG_FORCE_ANALOG, isForced);
+ }
+
+ @Override
+ public boolean isConfigFlagSupported(@RadioManager.ConfigFlag int flag) {
try {
- return mTuner.isAnalogForced();
+ return mTuner.isConfigFlagSupported(flag);
} catch (RemoteException e) {
throw new RuntimeException("service died", e);
}
}
@Override
- public void setAnalogForced(boolean isForced) {
+ public boolean isConfigFlagSet(@RadioManager.ConfigFlag int flag) {
try {
- mTuner.setAnalogForced(isForced);
+ return mTuner.isConfigFlagSet(flag);
+ } catch (RemoteException e) {
+ throw new RuntimeException("service died", e);
+ }
+ }
+
+ @Override
+ public void setConfigFlag(@RadioManager.ConfigFlag int flag, boolean value) {
+ try {
+ mTuner.setConfigFlag(flag, value);
} catch (RemoteException e) {
throw new RuntimeException("service died", e);
}
diff --git a/android/hardware/radio/TunerCallbackAdapter.java b/android/hardware/radio/TunerCallbackAdapter.java
index a01f658..b299ffe 100644
--- a/android/hardware/radio/TunerCallbackAdapter.java
+++ b/android/hardware/radio/TunerCallbackAdapter.java
@@ -22,7 +22,9 @@
import android.os.Looper;
import android.util.Log;
+import java.util.List;
import java.util.Map;
+import java.util.Objects;
/**
* Implements the ITunerCallback interface by forwarding calls to RadioTuner.Callback.
@@ -30,9 +32,14 @@
class TunerCallbackAdapter extends ITunerCallback.Stub {
private static final String TAG = "BroadcastRadio.TunerCallbackAdapter";
+ private final Object mLock = new Object();
@NonNull private final RadioTuner.Callback mCallback;
@NonNull private final Handler mHandler;
+ @Nullable ProgramList mProgramList;
+ @Nullable List<RadioManager.ProgramInfo> mLastCompleteList; // for legacy getProgramList call
+ private boolean mDelayedCompleteCallback = false;
+
TunerCallbackAdapter(@NonNull RadioTuner.Callback callback, @Nullable Handler handler) {
mCallback = callback;
if (handler == null) {
@@ -42,6 +49,49 @@
}
}
+ void setProgramListObserver(@Nullable ProgramList programList,
+ @NonNull ProgramList.OnCloseListener closeListener) {
+ Objects.requireNonNull(closeListener);
+ synchronized (mLock) {
+ if (mProgramList != null) {
+ Log.w(TAG, "Previous program list observer wasn't properly closed, closing it...");
+ mProgramList.close();
+ }
+ mProgramList = programList;
+ if (programList == null) return;
+ programList.setOnCloseListener(() -> {
+ synchronized (mLock) {
+ if (mProgramList != programList) return;
+ mProgramList = null;
+ mLastCompleteList = null;
+ closeListener.onClose();
+ }
+ });
+ programList.addOnCompleteListener(() -> {
+ synchronized (mLock) {
+ if (mProgramList != programList) return;
+ mLastCompleteList = programList.toList();
+ if (mDelayedCompleteCallback) {
+ Log.d(TAG, "Sending delayed onBackgroundScanComplete callback");
+ sendBackgroundScanCompleteLocked();
+ }
+ }
+ });
+ }
+ }
+
+ @Nullable List<RadioManager.ProgramInfo> getLastCompleteList() {
+ synchronized (mLock) {
+ return mLastCompleteList;
+ }
+ }
+
+ void clearLastCompleteList() {
+ synchronized (mLock) {
+ mLastCompleteList = null;
+ }
+ }
+
@Override
public void onError(int status) {
mHandler.post(() -> mCallback.onError(status));
@@ -87,9 +137,22 @@
mHandler.post(() -> mCallback.onBackgroundScanAvailabilityChange(isAvailable));
}
+ private void sendBackgroundScanCompleteLocked() {
+ mDelayedCompleteCallback = false;
+ mHandler.post(() -> mCallback.onBackgroundScanComplete());
+ }
+
@Override
public void onBackgroundScanComplete() {
- mHandler.post(() -> mCallback.onBackgroundScanComplete());
+ synchronized (mLock) {
+ if (mLastCompleteList == null) {
+ Log.i(TAG, "Got onBackgroundScanComplete callback, but the "
+ + "program list didn't get through yet. Delaying it...");
+ mDelayedCompleteCallback = true;
+ return;
+ }
+ sendBackgroundScanCompleteLocked();
+ }
}
@Override
@@ -98,6 +161,14 @@
}
@Override
+ public void onProgramListUpdated(ProgramList.Chunk chunk) {
+ synchronized (mLock) {
+ if (mProgramList == null) return;
+ mProgramList.apply(Objects.requireNonNull(chunk));
+ }
+ }
+
+ @Override
public void onParametersUpdated(Map parameters) {
mHandler.post(() -> mCallback.onParametersUpdated(parameters));
}
diff --git a/android/hardware/radio/Utils.java b/android/hardware/radio/Utils.java
new file mode 100644
index 0000000..f1b5897
--- /dev/null
+++ b/android/hardware/radio/Utils.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.radio;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+final class Utils {
+ private static final String TAG = "BroadcastRadio.utils";
+
+ static void writeStringMap(@NonNull Parcel dest, @Nullable Map<String, String> map) {
+ if (map == null) {
+ dest.writeInt(0);
+ return;
+ }
+ dest.writeInt(map.size());
+ for (Map.Entry<String, String> entry : map.entrySet()) {
+ dest.writeString(entry.getKey());
+ dest.writeString(entry.getValue());
+ }
+ }
+
+ static @NonNull Map<String, String> readStringMap(@NonNull Parcel in) {
+ int size = in.readInt();
+ Map<String, String> map = new HashMap<>();
+ while (size-- > 0) {
+ String key = in.readString();
+ String value = in.readString();
+ map.put(key, value);
+ }
+ return map;
+ }
+
+ static <T extends Parcelable> void writeSet(@NonNull Parcel dest, @Nullable Set<T> set) {
+ if (set == null) {
+ dest.writeInt(0);
+ return;
+ }
+ dest.writeInt(set.size());
+ set.stream().forEach(elem -> dest.writeTypedObject(elem, 0));
+ }
+
+ static <T> Set<T> createSet(@NonNull Parcel in, Parcelable.Creator<T> c) {
+ int size = in.readInt();
+ Set<T> set = new HashSet<>();
+ while (size-- > 0) {
+ set.add(in.readTypedObject(c));
+ }
+ return set;
+ }
+
+ static void writeIntSet(@NonNull Parcel dest, @Nullable Set<Integer> set) {
+ if (set == null) {
+ dest.writeInt(0);
+ return;
+ }
+ dest.writeInt(set.size());
+ set.stream().forEach(elem -> dest.writeInt(Objects.requireNonNull(elem)));
+ }
+
+ static Set<Integer> createIntSet(@NonNull Parcel in) {
+ return createSet(in, new Parcelable.Creator<Integer>() {
+ public Integer createFromParcel(Parcel in) {
+ return in.readInt();
+ }
+
+ public Integer[] newArray(int size) {
+ return new Integer[size];
+ }
+ });
+ }
+
+ static <T extends Parcelable> void writeTypedCollection(@NonNull Parcel dest,
+ @Nullable Collection<T> coll) {
+ ArrayList<T> list = null;
+ if (coll != null) {
+ if (coll instanceof ArrayList) {
+ list = (ArrayList) coll;
+ } else {
+ list = new ArrayList<>(coll);
+ }
+ }
+ dest.writeTypedList(list);
+ }
+
+ static void close(ICloseHandle handle) {
+ try {
+ handle.close();
+ } catch (RemoteException ex) {
+ ex.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/android/hardware/usb/UsbManager.java b/android/hardware/usb/UsbManager.java
index bdb90bc..7617c2b 100644
--- a/android/hardware/usb/UsbManager.java
+++ b/android/hardware/usb/UsbManager.java
@@ -601,6 +601,32 @@
}
/**
+ * Sets the screen unlocked functions, which are persisted and set as the current functions
+ * whenever the screen is unlocked.
+ * <p>
+ * The allowed values are: {@link #USB_FUNCTION_NONE},
+ * {@link #USB_FUNCTION_MIDI}, {@link #USB_FUNCTION_MTP}, {@link #USB_FUNCTION_PTP},
+ * or {@link #USB_FUNCTION_RNDIS}.
+ * {@link #USB_FUNCTION_NONE} has the effect of switching off this feature, so functions
+ * no longer change on screen unlock.
+ * </p><p>
+ * Note: When the screen is on, this method will apply given functions as current functions,
+ * which is asynchronous and may fail silently without applying the requested changes.
+ * </p>
+ *
+ * @param function function to set as default
+ *
+ * {@hide}
+ */
+ public void setScreenUnlockedFunctions(String function) {
+ try {
+ mService.setScreenUnlockedFunctions(function);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Returns a list of physical USB ports on the device.
* <p>
* This list is guaranteed to contain all dual-role USB Type C ports but it might
diff --git a/android/inputmethodservice/InputMethodService.java b/android/inputmethodservice/InputMethodService.java
index 02b1c65..7528bc3 100644
--- a/android/inputmethodservice/InputMethodService.java
+++ b/android/inputmethodservice/InputMethodService.java
@@ -18,6 +18,7 @@
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
import android.annotation.CallSuper;
import android.annotation.DrawableRes;
@@ -339,42 +340,35 @@
final Insets mTmpInsets = new Insets();
final int[] mTmpLocation = new int[2];
- final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
- new ViewTreeObserver.OnComputeInternalInsetsListener() {
- public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
- if (isExtractViewShown()) {
- // In true fullscreen mode, we just say the window isn't covering
- // any content so we don't impact whatever is behind.
- View decor = getWindow().getWindow().getDecorView();
- info.contentInsets.top = info.visibleInsets.top
- = decor.getHeight();
- info.touchableRegion.setEmpty();
- info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
- } else {
- onComputeInsets(mTmpInsets);
- info.contentInsets.top = mTmpInsets.contentTopInsets;
- info.visibleInsets.top = mTmpInsets.visibleTopInsets;
- info.touchableRegion.set(mTmpInsets.touchableRegion);
- info.setTouchableInsets(mTmpInsets.touchableInsets);
+ final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = info -> {
+ if (isExtractViewShown()) {
+ // In true fullscreen mode, we just say the window isn't covering
+ // any content so we don't impact whatever is behind.
+ View decor = getWindow().getWindow().getDecorView();
+ info.contentInsets.top = info.visibleInsets.top = decor.getHeight();
+ info.touchableRegion.setEmpty();
+ info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
+ } else {
+ onComputeInsets(mTmpInsets);
+ info.contentInsets.top = mTmpInsets.contentTopInsets;
+ info.visibleInsets.top = mTmpInsets.visibleTopInsets;
+ info.touchableRegion.set(mTmpInsets.touchableRegion);
+ info.setTouchableInsets(mTmpInsets.touchableInsets);
+ }
+ };
+
+ final View.OnClickListener mActionClickListener = v -> {
+ final EditorInfo ei = getCurrentInputEditorInfo();
+ final InputConnection ic = getCurrentInputConnection();
+ if (ei != null && ic != null) {
+ if (ei.actionId != 0) {
+ ic.performEditorAction(ei.actionId);
+ } else if ((ei.imeOptions & EditorInfo.IME_MASK_ACTION) != EditorInfo.IME_ACTION_NONE) {
+ ic.performEditorAction(ei.imeOptions & EditorInfo.IME_MASK_ACTION);
}
}
};
- final View.OnClickListener mActionClickListener = new View.OnClickListener() {
- public void onClick(View v) {
- final EditorInfo ei = getCurrentInputEditorInfo();
- final InputConnection ic = getCurrentInputConnection();
- if (ei != null && ic != null) {
- if (ei.actionId != 0) {
- ic.performEditorAction(ei.actionId);
- } else if ((ei.imeOptions&EditorInfo.IME_MASK_ACTION)
- != EditorInfo.IME_ACTION_NONE) {
- ic.performEditorAction(ei.imeOptions&EditorInfo.IME_MASK_ACTION);
- }
- }
- }
- };
-
/**
* Concrete implementation of
* {@link AbstractInputMethodService.AbstractInputMethodImpl} that provides
@@ -852,6 +846,11 @@
Context.LAYOUT_INFLATER_SERVICE);
mWindow = new SoftInputWindow(this, "InputMethod", mTheme, null, null, mDispatcherState,
WindowManager.LayoutParams.TYPE_INPUT_METHOD, Gravity.BOTTOM, false);
+ // For ColorView in DecorView to work, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS needs to be set
+ // by default (but IME developers can opt this out later if they want a new behavior).
+ mWindow.getWindow().setFlags(
+ FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+
initViews();
mWindow.getWindow().setLayout(MATCH_PARENT, WRAP_CONTENT);
}
@@ -882,8 +881,6 @@
mThemeAttrs = obtainStyledAttributes(android.R.styleable.InputMethodService);
mRootView = mInflater.inflate(
com.android.internal.R.layout.input_method, null);
- mRootView.setSystemUiVisibility(
- View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
mWindow.setContentView(mRootView);
mRootView.getViewTreeObserver().removeOnComputeInternalInsetsListener(mInsetsComputer);
mRootView.getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsComputer);
@@ -892,20 +889,20 @@
mWindow.getWindow().setWindowAnimations(
com.android.internal.R.style.Animation_InputMethodFancy);
}
- mFullscreenArea = (ViewGroup)mRootView.findViewById(com.android.internal.R.id.fullscreenArea);
+ mFullscreenArea = mRootView.findViewById(com.android.internal.R.id.fullscreenArea);
mExtractViewHidden = false;
- mExtractFrame = (FrameLayout)mRootView.findViewById(android.R.id.extractArea);
+ mExtractFrame = mRootView.findViewById(android.R.id.extractArea);
mExtractView = null;
mExtractEditText = null;
mExtractAccessories = null;
mExtractAction = null;
mFullscreenApplied = false;
-
- mCandidatesFrame = (FrameLayout)mRootView.findViewById(android.R.id.candidatesArea);
- mInputFrame = (FrameLayout)mRootView.findViewById(android.R.id.inputArea);
+
+ mCandidatesFrame = mRootView.findViewById(android.R.id.candidatesArea);
+ mInputFrame = mRootView.findViewById(android.R.id.inputArea);
mInputView = null;
mIsInputViewShown = false;
-
+
mExtractFrame.setVisibility(View.GONE);
mCandidatesVisibility = getCandidatesHiddenVisibility();
mCandidatesFrame.setVisibility(mCandidatesVisibility);
@@ -1085,33 +1082,6 @@
}
/**
- * Close/hide the input method's soft input area, so the user no longer
- * sees it or can interact with it. This can only be called
- * from the currently active input method, as validated by the given token.
- *
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link InputMethodManager#HIDE_IMPLICIT_ONLY},
- * {@link InputMethodManager#HIDE_NOT_ALWAYS} bit set.
- */
- public void hideSoftInputFromInputMethod(int flags) {
- mImm.hideSoftInputFromInputMethodInternal(mToken, flags);
- }
-
- /**
- * Show the input method's soft input area, so the user
- * sees the input method window and can interact with it.
- * This can only be called from the currently active input method,
- * as validated by the given token.
- *
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link InputMethodManager#SHOW_IMPLICIT} or
- * {@link InputMethodManager#SHOW_FORCED} bit set.
- */
- public void showSoftInputFromInputMethod(int flags) {
- mImm.showSoftInputFromInputMethodInternal(mToken, flags);
- }
-
- /**
* Force switch to the last used input method and subtype. If the last input method didn't have
* any subtypes, the framework will simply switch to the last input method with no subtype
* specified.
@@ -1457,17 +1427,17 @@
public int getCandidatesHiddenVisibility() {
return isExtractViewShown() ? View.GONE : View.INVISIBLE;
}
-
+
public void showStatusIcon(@DrawableRes int iconResId) {
mStatusIcon = iconResId;
- mImm.showStatusIcon(mToken, getPackageName(), iconResId);
+ mImm.showStatusIconInternal(mToken, getPackageName(), iconResId);
}
-
+
public void hideStatusIcon() {
mStatusIcon = 0;
- mImm.hideStatusIcon(mToken);
+ mImm.hideStatusIconInternal(mToken);
}
-
+
/**
* Force switch to a new input method, as identified by <var>id</var>. This
* input method will be destroyed, and the requested one started on the
@@ -1476,9 +1446,9 @@
* @param id Unique identifier of the new input method ot start.
*/
public void switchInputMethod(String id) {
- mImm.setInputMethod(mToken, id);
+ mImm.setInputMethodInternal(mToken, id);
}
-
+
public void setExtractView(View view) {
mExtractFrame.removeAllViews();
mExtractFrame.addView(view, new FrameLayout.LayoutParams(
@@ -1486,13 +1456,13 @@
ViewGroup.LayoutParams.MATCH_PARENT));
mExtractView = view;
if (view != null) {
- mExtractEditText = (ExtractEditText)view.findViewById(
+ mExtractEditText = view.findViewById(
com.android.internal.R.id.inputExtractEditText);
mExtractEditText.setIME(this);
mExtractAction = view.findViewById(
com.android.internal.R.id.inputExtractAction);
if (mExtractAction != null) {
- mExtractAccessories = (ViewGroup)view.findViewById(
+ mExtractAccessories = view.findViewById(
com.android.internal.R.id.inputExtractAccessories);
}
startExtractingText(false);
@@ -1741,7 +1711,7 @@
// Rethrow the exception to preserve the existing behavior. Some IMEs may have directly
// called this method and relied on this exception for some clean-up tasks.
// TODO: Give developers a clear guideline of whether it's OK to call this method or
- // InputMethodManager#showSoftInputFromInputMethod() should always be used instead.
+ // InputMethodService#requestShowSelf(int) should always be used instead.
throw e;
} finally {
// TODO: Is it OK to set true when we get BadTokenException?
@@ -2063,27 +2033,30 @@
/**
* Close this input method's soft input area, removing it from the display.
- * The input method will continue running, but the user can no longer use
- * it to generate input by touching the screen.
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link InputMethodManager#HIDE_IMPLICIT_ONLY
- * InputMethodManager.HIDE_IMPLICIT_ONLY} bit set.
+ *
+ * The input method will continue running, but the user can no longer use it to generate input
+ * by touching the screen.
+ *
+ * @see InputMethodManager#HIDE_IMPLICIT_ONLY
+ * @see InputMethodManager#HIDE_NOT_ALWAYS
+ * @param flags Provides additional operating flags.
*/
public void requestHideSelf(int flags) {
- mImm.hideSoftInputFromInputMethod(mToken, flags);
+ mImm.hideSoftInputFromInputMethodInternal(mToken, flags);
}
-
+
/**
- * Show the input method. This is a call back to the
- * IMF to handle showing the input method.
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link InputMethodManager#SHOW_FORCED
- * InputMethodManager.} bit set.
+ * Show the input method's soft input area, so the user sees the input method window and can
+ * interact with it.
+ *
+ * @see InputMethodManager#SHOW_IMPLICIT
+ * @see InputMethodManager#SHOW_FORCED
+ * @param flags Provides additional operating flags.
*/
- private void requestShowSelf(int flags) {
- mImm.showSoftInputFromInputMethod(mToken, flags);
+ public void requestShowSelf(int flags) {
+ mImm.showSoftInputFromInputMethodInternal(mToken, flags);
}
-
+
private boolean handleBack(boolean doIt) {
if (mShowInputRequested) {
// If the soft input area is shown, back closes it and we
@@ -2750,7 +2723,7 @@
* application.
* This cannot be {@code null}.
* @param inputConnection {@link InputConnection} with which
- * {@link InputConnection#commitContent(InputContentInfo, Bundle)} will be called.
+ * {@link InputConnection#commitContent(InputContentInfo, int, Bundle)} will be called.
* @hide
*/
@Override
diff --git a/android/location/LocationManager.java b/android/location/LocationManager.java
index 4802b23..9db9d33 100644
--- a/android/location/LocationManager.java
+++ b/android/location/LocationManager.java
@@ -16,7 +16,10 @@
package android.location;
-import com.android.internal.location.ProviderProperties;
+import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.Manifest.permission.LOCATION_HARDWARE;
+import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
import android.Manifest;
import android.annotation.NonNull;
@@ -24,7 +27,6 @@
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
-import android.annotation.TestApi;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
@@ -33,16 +35,15 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.Process;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.util.Log;
-
+import com.android.internal.location.ProviderProperties;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
-import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
-import static android.Manifest.permission.ACCESS_FINE_LOCATION;
-
/**
* This class provides access to the system location services. These
* services allow applications to obtain periodic updates of the
@@ -882,6 +883,34 @@
requestLocationUpdates(request, null, null, intent);
}
+ /**
+ * Set the last known location with a new location.
+ *
+ * <p>A privileged client can inject a {@link Location} if it has a better estimate of what
+ * the recent location is. This is especially useful when the device boots up and the GPS
+ * chipset is in the process of getting the first fix. If the client has cached the location,
+ * it can inject the {@link Location}, so if an app requests for a {@link Location} from {@link
+ * #getLastKnownLocation(String)}, the location information is still useful before getting
+ * the first fix.</p>
+ *
+ * <p> Useful in products like Auto.
+ *
+ * @param newLocation newly available {@link Location} object
+ * @return true if update was successful, false if not
+ *
+ * @throws SecurityException if no suitable permission is present
+ *
+ * @hide
+ */
+ @RequiresPermission(allOf = {LOCATION_HARDWARE, ACCESS_FINE_LOCATION})
+ public boolean injectLocation(Location newLocation) {
+ try {
+ return mService.injectLocation(newLocation);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
private ListenerTransport wrapListener(LocationListener listener, Looper looper) {
if (listener == null) return null;
synchronized (mListeners) {
@@ -1142,13 +1171,57 @@
}
/**
+ * Returns the current enabled/disabled status of location
+ *
+ * @return true if location is enabled. false if location is disabled.
+ */
+ public boolean isLocationEnabled() {
+ return isLocationEnabledForUser(Process.myUserHandle());
+ }
+
+ /**
+ * Method for enabling or disabling location.
+ *
+ * @param enabled true to enable location. false to disable location
+ * @param userHandle the user to set
+ * @return true if the value was set, false on database errors
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(WRITE_SECURE_SETTINGS)
+ public void setLocationEnabledForUser(boolean enabled, UserHandle userHandle) {
+ try {
+ mService.setLocationEnabledForUser(enabled, userHandle.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the current enabled/disabled status of location
+ *
+ * @param userHandle the user to query
+ * @return true location is enabled. false if location is disabled.
+ *
+ * @hide
+ */
+ @SystemApi
+ public boolean isLocationEnabledForUser(UserHandle userHandle) {
+ try {
+ return mService.isLocationEnabledForUser(userHandle.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Returns the current enabled/disabled status of the given provider.
*
* <p>If the user has enabled this provider in the Settings menu, true
* is returned otherwise false is returned
*
- * <p>Callers should instead use
- * {@link android.provider.Settings.Secure#LOCATION_MODE}
+ * <p>Callers should instead use {@link #isLocationEnabled()}
* unless they depend on provider-specific APIs such as
* {@link #requestLocationUpdates(String, long, float, LocationListener)}.
*
@@ -1173,6 +1246,64 @@
}
/**
+ * Returns the current enabled/disabled status of the given provider and user.
+ *
+ * <p>If the user has enabled this provider in the Settings menu, true
+ * is returned otherwise false is returned
+ *
+ * <p>Callers should instead use {@link #isLocationEnabled()}
+ * unless they depend on provider-specific APIs such as
+ * {@link #requestLocationUpdates(String, long, float, LocationListener)}.
+ *
+ * <p>
+ * Before API version {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this
+ * method would throw {@link SecurityException} if the location permissions
+ * were not sufficient to use the specified provider.
+ *
+ * @param provider the name of the provider
+ * @param userHandle the user to query
+ * @return true if the provider exists and is enabled
+ *
+ * @throws IllegalArgumentException if provider is null
+ * @hide
+ */
+ @SystemApi
+ public boolean isProviderEnabledForUser(String provider, UserHandle userHandle) {
+ checkProvider(provider);
+
+ try {
+ return mService.isProviderEnabledForUser(provider, userHandle.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Method for enabling or disabling a single location provider.
+ *
+ * @param provider the name of the provider
+ * @param enabled true to enable the provider. false to disable the provider
+ * @param userHandle the user to set
+ * @return true if the value was set, false on database errors
+ *
+ * @throws IllegalArgumentException if provider is null
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(WRITE_SECURE_SETTINGS)
+ public boolean setProviderEnabledForUser(
+ String provider, boolean enabled, UserHandle userHandle) {
+ checkProvider(provider);
+
+ try {
+ return mService.setProviderEnabledForUser(
+ provider, enabled, userHandle.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Get the last known location.
*
* <p>This location could be very old so use
diff --git a/android/media/AudioAttributes.java b/android/media/AudioAttributes.java
index e0289f0..44a2ff9 100644
--- a/android/media/AudioAttributes.java
+++ b/android/media/AudioAttributes.java
@@ -180,6 +180,7 @@
/**
* IMPORTANT: when adding new usage types, add them to SDK_USAGES and update SUPPRESSIBLE_USAGES
* if applicable, as well as audioattributes.proto.
+ * Also consider adding them to <aaudio/AAudio.h> for the NDK.
*/
/**
@@ -879,7 +880,9 @@
}
/** @hide */
- public void toProto(ProtoOutputStream proto) {
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
proto.write(AudioAttributesProto.USAGE, mUsage);
proto.write(AudioAttributesProto.CONTENT_TYPE, mContentType);
proto.write(AudioAttributesProto.FLAGS, mFlags);
@@ -891,6 +894,8 @@
}
}
// TODO: is the data in mBundle useful for debugging?
+
+ proto.end(token);
}
/** @hide */
diff --git a/android/media/AudioFocusInfo.java b/android/media/AudioFocusInfo.java
index 6d9c5e2..5d0c8e2 100644
--- a/android/media/AudioFocusInfo.java
+++ b/android/media/AudioFocusInfo.java
@@ -130,13 +130,11 @@
dest.writeInt(mSdkTarget);
}
- @SystemApi
@Override
public int hashCode() {
return Objects.hash(mAttributes, mClientUid, mClientId, mPackageName, mGainRequest, mFlags);
}
- @SystemApi
@Override
public boolean equals(Object obj) {
if (this == obj)
diff --git a/android/media/AudioFocusRequest.java b/android/media/AudioFocusRequest.java
index de59ac3..7104dad 100644
--- a/android/media/AudioFocusRequest.java
+++ b/android/media/AudioFocusRequest.java
@@ -20,6 +20,7 @@
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -220,6 +221,9 @@
private final static AudioAttributes FOCUS_DEFAULT_ATTR = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA).build();
+ /** @hide */
+ public static final String KEY_ACCESSIBILITY_FORCE_FOCUS_DUCKING = "a11y_force_ducking";
+
private final OnAudioFocusChangeListener mFocusListener; // may be null
private final Handler mListenerHandler; // may be null
private final AudioAttributes mAttr; // never null
@@ -349,6 +353,7 @@
private boolean mPausesOnDuck = false;
private boolean mDelayedFocus = false;
private boolean mFocusLocked = false;
+ private boolean mA11yForceDucking = false;
/**
* Constructs a new {@code Builder}, and specifies how audio focus
@@ -526,6 +531,21 @@
}
/**
+ * Marks this focus request as forcing ducking, regardless of the conditions in which
+ * the system would or would not enforce ducking.
+ * Forcing ducking will only be honored when requesting AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+ * with an {@link AudioAttributes} usage of
+ * {@link AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY}, coming from an accessibility
+ * service, and will be ignored otherwise.
+ * @param forceDucking {@code true} to force ducking
+ * @return this {@code Builder} instance
+ */
+ public @NonNull Builder setForceDucking(boolean forceDucking) {
+ mA11yForceDucking = forceDucking;
+ return this;
+ }
+
+ /**
* Builds a new {@code AudioFocusRequest} instance combining all the information gathered
* by this {@code Builder}'s configuration methods.
* @return the {@code AudioFocusRequest} instance qualified by all the properties set
@@ -538,6 +558,17 @@
throw new IllegalStateException(
"Can't use delayed focus or pause on duck without a listener");
}
+ if (mA11yForceDucking) {
+ final Bundle extraInfo;
+ if (mAttr.getBundle() == null) {
+ extraInfo = new Bundle();
+ } else {
+ extraInfo = mAttr.getBundle();
+ }
+ // checking of usage and focus request is done server side
+ extraInfo.putBoolean(KEY_ACCESSIBILITY_FORCE_FOCUS_DUCKING, true);
+ mAttr = new AudioAttributes.Builder(mAttr).addBundle(extraInfo).build();
+ }
final int flags = 0
| (mDelayedFocus ? AudioManager.AUDIOFOCUS_FLAG_DELAY_OK : 0)
| (mPausesOnDuck ? AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS : 0)
diff --git a/android/media/AudioFormat.java b/android/media/AudioFormat.java
index 93fc3da..b07d042 100644
--- a/android/media/AudioFormat.java
+++ b/android/media/AudioFormat.java
@@ -238,22 +238,15 @@
public static final int ENCODING_DTS = 7;
/** Audio data format: DTS HD compressed */
public static final int ENCODING_DTS_HD = 8;
- /** Audio data format: MP3 compressed
- * @hide
- * */
+ /** Audio data format: MP3 compressed */
public static final int ENCODING_MP3 = 9;
- /** Audio data format: AAC LC compressed
- * @hide
- * */
+ /** Audio data format: AAC LC compressed */
public static final int ENCODING_AAC_LC = 10;
- /** Audio data format: AAC HE V1 compressed
- * @hide
- * */
+ /** Audio data format: AAC HE V1 compressed */
public static final int ENCODING_AAC_HE_V1 = 11;
- /** Audio data format: AAC HE V2 compressed
- * @hide
- * */
+ /** Audio data format: AAC HE V2 compressed */
public static final int ENCODING_AAC_HE_V2 = 12;
+
/** Audio data format: compressed audio wrapped in PCM for HDMI
* or S/PDIF passthrough.
* IEC61937 uses a stereo stream of 16-bit samples as the wrapper.
@@ -266,6 +259,12 @@
/** Audio data format: DOLBY TRUEHD compressed
**/
public static final int ENCODING_DOLBY_TRUEHD = 14;
+ /** Audio data format: AAC ELD compressed */
+ public static final int ENCODING_AAC_ELD = 15;
+ /** Audio data format: AAC xHE compressed */
+ public static final int ENCODING_AAC_XHE = 16;
+ /** Audio data format: AC-4 sync frame transport format */
+ public static final int ENCODING_AC4 = 17;
/** @hide */
public static String toLogFriendlyEncoding(int enc) {
@@ -298,6 +297,12 @@
return "ENCODING_IEC61937";
case ENCODING_DOLBY_TRUEHD:
return "ENCODING_DOLBY_TRUEHD";
+ case ENCODING_AAC_ELD:
+ return "ENCODING_AAC_ELD";
+ case ENCODING_AAC_XHE:
+ return "ENCODING_AAC_XHE";
+ case ENCODING_AC4:
+ return "ENCODING_AC4";
default :
return "invalid encoding " + enc;
}
@@ -514,6 +519,9 @@
case ENCODING_AAC_HE_V1:
case ENCODING_AAC_HE_V2:
case ENCODING_IEC61937:
+ case ENCODING_AAC_ELD:
+ case ENCODING_AAC_XHE:
+ case ENCODING_AC4:
return true;
default:
return false;
@@ -532,6 +540,13 @@
case ENCODING_DTS:
case ENCODING_DTS_HD:
case ENCODING_IEC61937:
+ case ENCODING_MP3:
+ case ENCODING_AAC_LC:
+ case ENCODING_AAC_HE_V1:
+ case ENCODING_AAC_HE_V2:
+ case ENCODING_AAC_ELD:
+ case ENCODING_AAC_XHE:
+ case ENCODING_AC4:
return true;
default:
return false;
@@ -556,6 +571,9 @@
case ENCODING_AAC_HE_V1:
case ENCODING_AAC_HE_V2:
case ENCODING_IEC61937: // wrapped in PCM but compressed
+ case ENCODING_AAC_ELD:
+ case ENCODING_AAC_XHE:
+ case ENCODING_AC4:
return false;
case ENCODING_INVALID:
default:
@@ -581,6 +599,9 @@
case ENCODING_AAC_LC:
case ENCODING_AAC_HE_V1:
case ENCODING_AAC_HE_V2:
+ case ENCODING_AAC_ELD:
+ case ENCODING_AAC_XHE:
+ case ENCODING_AC4:
return false;
case ENCODING_INVALID:
default:
@@ -794,14 +815,7 @@
/**
* Sets the data encoding format.
- * @param encoding one of {@link AudioFormat#ENCODING_DEFAULT},
- * {@link AudioFormat#ENCODING_PCM_8BIT},
- * {@link AudioFormat#ENCODING_PCM_16BIT},
- * {@link AudioFormat#ENCODING_PCM_FLOAT},
- * {@link AudioFormat#ENCODING_AC3},
- * {@link AudioFormat#ENCODING_E_AC3}.
- * {@link AudioFormat#ENCODING_DTS},
- * {@link AudioFormat#ENCODING_DTS_HD}.
+ * @param encoding the specified encoding or default.
* @return the same Builder instance.
* @throws java.lang.IllegalArgumentException
*/
@@ -818,6 +832,13 @@
case ENCODING_DTS:
case ENCODING_DTS_HD:
case ENCODING_IEC61937:
+ case ENCODING_MP3:
+ case ENCODING_AAC_LC:
+ case ENCODING_AAC_HE_V1:
+ case ENCODING_AAC_HE_V2:
+ case ENCODING_AAC_ELD:
+ case ENCODING_AAC_XHE:
+ case ENCODING_AC4:
mEncoding = encoding;
break;
case ENCODING_INVALID:
@@ -1016,7 +1037,7 @@
}
/** @hide */
- @IntDef({
+ @IntDef(flag = false, prefix = "ENCODING", value = {
ENCODING_DEFAULT,
ENCODING_PCM_8BIT,
ENCODING_PCM_16BIT,
@@ -1025,8 +1046,14 @@
ENCODING_E_AC3,
ENCODING_DTS,
ENCODING_DTS_HD,
- ENCODING_IEC61937
- })
+ ENCODING_IEC61937,
+ ENCODING_AAC_HE_V1,
+ ENCODING_AAC_HE_V2,
+ ENCODING_AAC_LC,
+ ENCODING_AAC_ELD,
+ ENCODING_AAC_XHE,
+ ENCODING_AC4 }
+ )
@Retention(RetentionPolicy.SOURCE)
public @interface Encoding {}
diff --git a/android/media/AudioManager.java b/android/media/AudioManager.java
index 913b5e8..2ac4063 100644
--- a/android/media/AudioManager.java
+++ b/android/media/AudioManager.java
@@ -1329,6 +1329,19 @@
}
//====================================================================
+ // Offload query
+ /**
+ * Returns whether offloaded playback of an audio format is supported on the device.
+ * Offloaded playback is where the decoding of an audio stream is not competing with other
+ * software resources. In general, it is supported by dedicated hardware, such as audio DSPs.
+ * @param format the audio format (codec, sample rate, channels) being checked.
+ * @return true if the given audio format can be offloaded.
+ */
+ public boolean isOffloadedPlaybackSupported(@NonNull AudioFormat format) {
+ return AudioSystem.isOffloadSupported(format);
+ }
+
+ //====================================================================
// Bluetooth SCO control
/**
* Sticky broadcast intent action indicating that the Bluetooth SCO audio
@@ -3746,6 +3759,33 @@
}
/**
+ * Indicate A2DP source or sink connection state change and eventually suppress
+ * the {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent.
+ * @param device Bluetooth device connected/disconnected
+ * @param state new connection state (BluetoothProfile.STATE_xxx)
+ * @param profile profile for the A2DP device
+ * (either {@link android.bluetooth.BluetoothProfile.A2DP} or
+ * {@link android.bluetooth.BluetoothProfile.A2DP_SINK})
+ * @param suppressNoisyIntent if true the
+ * {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent will not be sent.
+ * @return a delay in ms that the caller should wait before broadcasting
+ * BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED intent.
+ * {@hide}
+ */
+ public int setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
+ BluetoothDevice device, int state, int profile, boolean suppressNoisyIntent) {
+ final IAudioService service = getService();
+ int delay = 0;
+ try {
+ delay = service.setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(device,
+ state, profile, suppressNoisyIntent);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return delay;
+ }
+
+ /**
* Indicate A2DP device configuration has changed.
* @param device Bluetooth device whose configuration has changed.
* {@hide}
diff --git a/android/media/AudioPort.java b/android/media/AudioPort.java
index 19bf51d..047db19 100644
--- a/android/media/AudioPort.java
+++ b/android/media/AudioPort.java
@@ -20,7 +20,7 @@
* An audio port is a node of the audio framework or hardware that can be connected to or
* disconnect from another audio node to create a specific audio routing configuration.
* Examples of audio ports are an output device (speaker) or an output mix (see AudioMixPort).
- * All attributes that are relevant for applications to make routing selection are decribed
+ * All attributes that are relevant for applications to make routing selection are described
* in an AudioPort, in particular:
* - possible channel mask configurations.
* - audio format (PCM 16bit, PCM 24bit...)
@@ -173,6 +173,7 @@
/**
* Build a specific configuration of this audio port for use by methods
* like AudioManager.connectAudioPatch().
+ * @param samplingRate
* @param channelMask The desired channel mask. AudioFormat.CHANNEL_OUT_DEFAULT if no change
* from active configuration requested.
* @param format The desired audio format. AudioFormat.ENCODING_DEFAULT if no change
diff --git a/android/media/AudioSystem.java b/android/media/AudioSystem.java
index e56944d..dcd37da 100644
--- a/android/media/AudioSystem.java
+++ b/android/media/AudioSystem.java
@@ -16,6 +16,7 @@
package android.media;
+import android.annotation.NonNull;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.audiopolicy.AudioMix;
@@ -792,7 +793,7 @@
public static native int getPrimaryOutputFrameCount();
public static native int getOutputLatency(int stream);
- public static native int setLowRamDevice(boolean isLowRamDevice);
+ public static native int setLowRamDevice(boolean isLowRamDevice, long totalMemory);
public static native int checkAudioFlinger();
public static native int listAudioPorts(ArrayList<AudioPort> ports, int[] generation);
@@ -818,6 +819,14 @@
public static native float getStreamVolumeDB(int stream, int index, int device);
+ static boolean isOffloadSupported(@NonNull AudioFormat format) {
+ return native_is_offload_supported(format.getEncoding(), format.getSampleRate(),
+ format.getChannelMask(), format.getChannelIndexMask());
+ }
+
+ private static native boolean native_is_offload_supported(int encoding, int sampleRate,
+ int channelMask, int channelIndexMask);
+
// Items shared with audio service
/**
@@ -914,7 +923,8 @@
(1 << STREAM_MUSIC) |
(1 << STREAM_RING) |
(1 << STREAM_NOTIFICATION) |
- (1 << STREAM_SYSTEM);
+ (1 << STREAM_SYSTEM) |
+ (1 << STREAM_VOICE_CALL);
/**
* Event posted by AudioTrack and AudioRecord JNI (JNIDeviceCallback) when routing changes.
diff --git a/android/media/AudioTrack.java b/android/media/AudioTrack.java
index e535fdf..5928d03 100644
--- a/android/media/AudioTrack.java
+++ b/android/media/AudioTrack.java
@@ -24,7 +24,9 @@
import java.nio.ByteOrder;
import java.nio.NioUtils;
import java.util.Collection;
+import java.util.concurrent.Executor;
+import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -185,6 +187,22 @@
* Event id denotes when previously set update period has elapsed during playback.
*/
private static final int NATIVE_EVENT_NEW_POS = 4;
+ /**
+ * Callback for more data
+ * TODO only for offload
+ */
+ private static final int NATIVE_EVENT_MORE_DATA = 0;
+ /**
+ * IAudioTrack tear down for offloaded tracks
+ * TODO: when received, java AudioTrack must be released
+ */
+ private static final int NATIVE_EVENT_NEW_IAUDIOTRACK = 6;
+ /**
+ * Event id denotes when all the buffers queued in AF and HW are played
+ * back (after stop is called) for an offloaded track.
+ * TODO: not just for offload
+ */
+ private static final int NATIVE_EVENT_STREAM_END = 7;
private final static String TAG = "android.media.AudioTrack";
@@ -540,6 +558,12 @@
public AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
int mode, int sessionId)
throws IllegalArgumentException {
+ this(attributes, format, bufferSizeInBytes, mode, sessionId, false /*offload*/);
+ }
+
+ private AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
+ int mode, int sessionId, boolean offload)
+ throws IllegalArgumentException {
super(attributes, AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK);
// mState already == STATE_UNINITIALIZED
@@ -601,7 +625,8 @@
// native initialization
int initResult = native_setup(new WeakReference<AudioTrack>(this), mAttributes,
sampleRate, mChannelMask, mChannelIndexMask, mAudioFormat,
- mNativeBufferSizeInBytes, mDataLoadMode, session, 0 /*nativeTrackInJavaObj*/);
+ mNativeBufferSizeInBytes, mDataLoadMode, session, 0 /*nativeTrackInJavaObj*/,
+ offload);
if (initResult != SUCCESS) {
loge("Error code "+initResult+" when initializing AudioTrack.");
return; // with mState == STATE_UNINITIALIZED
@@ -681,7 +706,8 @@
0 /*mNativeBufferSizeInBytes - NA*/,
0 /*mDataLoadMode - NA*/,
session,
- nativeTrackInJavaObj);
+ nativeTrackInJavaObj,
+ false /*offload*/);
if (initResult != SUCCESS) {
loge("Error code "+initResult+" when initializing AudioTrack.");
return; // with mState == STATE_UNINITIALIZED
@@ -729,6 +755,7 @@
* <code>MODE_STREAM</code> will be used.
* <br>If the session ID is not specified with {@link #setSessionId(int)}, a new one will
* be generated.
+ * <br>Offload is false by default.
*/
public static class Builder {
private AudioAttributes mAttributes;
@@ -737,6 +764,7 @@
private int mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE;
private int mMode = MODE_STREAM;
private int mPerformanceMode = PERFORMANCE_MODE_NONE;
+ private boolean mOffload = false;
/**
* Constructs a new Builder with the default values as described above.
@@ -867,6 +895,21 @@
}
/**
+ * Sets whether this track will play through the offloaded audio path.
+ * When set to true, at build time, the audio format will be checked against
+ * {@link AudioManager#isOffloadedPlaybackSupported(AudioFormat)} to verify the audio format
+ * used by this track is supported on the device's offload path (if any).
+ * <br>Offload is only supported for media audio streams, and therefore requires that
+ * the usage be {@link AudioAttributes#USAGE_MEDIA}.
+ * @param offload true to require the offload path for playback.
+ * @return the same Builder instance.
+ */
+ public @NonNull Builder setOffloadedPlayback(boolean offload) {
+ mOffload = offload;
+ return this;
+ }
+
+ /**
* Builds an {@link AudioTrack} instance initialized with all the parameters set
* on this <code>Builder</code>.
* @return a new successfully initialized {@link AudioTrack} instance.
@@ -909,6 +952,19 @@
.setEncoding(AudioFormat.ENCODING_DEFAULT)
.build();
}
+
+ //TODO tie offload to PERFORMANCE_MODE_POWER_SAVING?
+ if (mOffload) {
+ if (mAttributes.getUsage() != AudioAttributes.USAGE_MEDIA) {
+ throw new UnsupportedOperationException(
+ "Cannot create AudioTrack, offload requires USAGE_MEDIA");
+ }
+ if (!AudioSystem.isOffloadSupported(mFormat)) {
+ throw new UnsupportedOperationException(
+ "Cannot create AudioTrack, offload format not supported");
+ }
+ }
+
try {
// If the buffer size is not specified in streaming mode,
// use a single frame for the buffer size and let the
@@ -918,7 +974,7 @@
* mFormat.getBytesPerSample(mFormat.getEncoding());
}
final AudioTrack track = new AudioTrack(
- mAttributes, mFormat, mBufferSizeInBytes, mMode, mSessionId);
+ mAttributes, mFormat, mBufferSizeInBytes, mMode, mSessionId, mOffload);
if (track.getState() == STATE_UNINITIALIZED) {
// release is not necessary
throw new UnsupportedOperationException("Cannot create AudioTrack");
@@ -2882,6 +2938,69 @@
void onPeriodicNotification(AudioTrack track);
}
+ /**
+ * Abstract class to receive event notification about the stream playback.
+ * See {@link AudioTrack#setStreamEventCallback(Executor, StreamEventCallback)} to register
+ * the callback on the given {@link AudioTrack} instance.
+ */
+ public abstract static class StreamEventCallback {
+ /** @hide */ // add hidden empty constructor so it doesn't show in SDK
+ public StreamEventCallback() { }
+ /**
+ * Called when an offloaded track is no longer valid and has been discarded by the system.
+ * An example of this happening is when an offloaded track has been paused too long, and
+ * gets invalidated by the system to prevent any other offload.
+ * @param track the {@link AudioTrack} on which the event happened
+ */
+ public void onTearDown(AudioTrack track) { }
+ /**
+ * Called when all the buffers of an offloaded track that were queued in the audio system
+ * (e.g. the combination of the Android audio framework and the device's audio hardware)
+ * have been played after {@link AudioTrack#stop()} has been called.
+ * @param track the {@link AudioTrack} on which the event happened
+ */
+ public void onStreamPresentationEnd(AudioTrack track) { }
+ /**
+ * Called when more audio data can be written without blocking on an offloaded track.
+ * @param track the {@link AudioTrack} on which the event happened
+ */
+ public void onStreamDataRequest(AudioTrack track) { }
+ }
+
+ private Executor mStreamEventExec;
+ private StreamEventCallback mStreamEventCb;
+ private final Object mStreamEventCbLock = new Object();
+
+ /**
+ * Sets the callback for the notification of stream events.
+ * @param executor {@link Executor} to handle the callbacks
+ * @param eventCallback the callback to receive the stream event notifications
+ */
+ public void setStreamEventCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull StreamEventCallback eventCallback) {
+ if (eventCallback == null) {
+ throw new IllegalArgumentException("Illegal null StreamEventCallback");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Illegal null Executor for the StreamEventCallback");
+ }
+ synchronized (mStreamEventCbLock) {
+ mStreamEventExec = executor;
+ mStreamEventCb = eventCallback;
+ }
+ }
+
+ /**
+ * Unregisters the callback for notification of stream events, previously set
+ * by {@link #setStreamEventCallback(Executor, StreamEventCallback)}.
+ */
+ public void removeStreamEventCallback() {
+ synchronized (mStreamEventCbLock) {
+ mStreamEventExec = null;
+ mStreamEventCb = null;
+ }
+ }
+
//---------------------------------------------------------
// Inner classes
//--------------------
@@ -2965,7 +3084,7 @@
private static void postEventFromNative(Object audiotrack_ref,
int what, int arg1, int arg2, Object obj) {
//logd("Event posted from the native side: event="+ what + " args="+ arg1+" "+arg2);
- AudioTrack track = (AudioTrack)((WeakReference)audiotrack_ref).get();
+ final AudioTrack track = (AudioTrack)((WeakReference)audiotrack_ref).get();
if (track == null) {
return;
}
@@ -2974,6 +3093,32 @@
track.broadcastRoutingChange();
return;
}
+
+ if (what == NATIVE_EVENT_MORE_DATA || what == NATIVE_EVENT_NEW_IAUDIOTRACK
+ || what == NATIVE_EVENT_STREAM_END) {
+ final Executor exec;
+ final StreamEventCallback cb;
+ synchronized (track.mStreamEventCbLock) {
+ exec = track.mStreamEventExec;
+ cb = track.mStreamEventCb;
+ }
+ if ((exec == null) || (cb == null)) {
+ return;
+ }
+ switch (what) {
+ case NATIVE_EVENT_MORE_DATA:
+ exec.execute(() -> cb.onStreamDataRequest(track));
+ return;
+ case NATIVE_EVENT_NEW_IAUDIOTRACK:
+ // TODO also release track as it's not longer usable
+ exec.execute(() -> cb.onTearDown(track));
+ return;
+ case NATIVE_EVENT_STREAM_END:
+ exec.execute(() -> cb.onStreamPresentationEnd(track));
+ return;
+ }
+ }
+
NativePositionEventHandlerDelegate delegate = track.mEventHandlerDelegate;
if (delegate != null) {
Handler handler = delegate.getHandler();
@@ -2995,7 +3140,8 @@
private native final int native_setup(Object /*WeakReference<AudioTrack>*/ audiotrack_this,
Object /*AudioAttributes*/ attributes,
int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
- int buffSizeInBytes, int mode, int[] sessionId, long nativeAudioTrack);
+ int buffSizeInBytes, int mode, int[] sessionId, long nativeAudioTrack,
+ boolean offload);
private native final void native_finalize();
diff --git a/android/media/DataSourceDesc.java b/android/media/DataSourceDesc.java
new file mode 100644
index 0000000..73fad7a
--- /dev/null
+++ b/android/media/DataSourceDesc.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.FileDescriptor;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.HttpCookie;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Structure for data source descriptor.
+ *
+ * Used by {@link MediaPlayer2#setDataSource(DataSourceDesc)}
+ * to set data source for playback.
+ *
+ * <p>Users should use {@link Builder} to change {@link DataSourceDesc}.
+ *
+ */
+public final class DataSourceDesc {
+ /* No data source has been set yet */
+ public static final int TYPE_NONE = 0;
+ /* data source is type of MediaDataSource */
+ public static final int TYPE_CALLBACK = 1;
+ /* data source is type of FileDescriptor */
+ public static final int TYPE_FD = 2;
+ /* data source is type of Uri */
+ public static final int TYPE_URI = 3;
+
+ // intentionally less than long.MAX_VALUE
+ public static final long LONG_MAX = 0x7ffffffffffffffL;
+
+ private int mType = TYPE_NONE;
+
+ private Media2DataSource mMedia2DataSource;
+
+ private FileDescriptor mFD;
+ private long mFDOffset = 0;
+ private long mFDLength = LONG_MAX;
+
+ private Uri mUri;
+ private Map<String, String> mUriHeader;
+ private List<HttpCookie> mUriCookies;
+ private Context mUriContext;
+
+ private long mId = 0;
+ private long mStartPositionMs = 0;
+ private long mEndPositionMs = LONG_MAX;
+
+ private DataSourceDesc() {
+ }
+
+ /**
+ * Return the Id of data source.
+ * @return the Id of data source
+ */
+ public long getId() {
+ return mId;
+ }
+
+ /**
+ * Return the position in milliseconds at which the playback will start.
+ * @return the position in milliseconds at which the playback will start
+ */
+ public long getStartPosition() {
+ return mStartPositionMs;
+ }
+
+ /**
+ * Return the position in milliseconds at which the playback will end.
+ * -1 means ending at the end of source content.
+ * @return the position in milliseconds at which the playback will end
+ */
+ public long getEndPosition() {
+ return mEndPositionMs;
+ }
+
+ /**
+ * Return the type of data source.
+ * @return the type of data source
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Return the Media2DataSource of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_CALLBACK}.
+ * @return the Media2DataSource of this data source
+ */
+ public Media2DataSource getMedia2DataSource() {
+ return mMedia2DataSource;
+ }
+
+ /**
+ * Return the FileDescriptor of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_FD}.
+ * @return the FileDescriptor of this data source
+ */
+ public FileDescriptor getFileDescriptor() {
+ return mFD;
+ }
+
+ /**
+ * Return the offset associated with the FileDescriptor of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_FD} and it has
+ * been set by the {@link Builder}.
+ * @return the offset associated with the FileDescriptor of this data source
+ */
+ public long getFileDescriptorOffset() {
+ return mFDOffset;
+ }
+
+ /**
+ * Return the content length associated with the FileDescriptor of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_FD}.
+ * -1 means same as the length of source content.
+ * @return the content length associated with the FileDescriptor of this data source
+ */
+ public long getFileDescriptorLength() {
+ return mFDLength;
+ }
+
+ /**
+ * Return the Uri of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
+ * @return the Uri of this data source
+ */
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * Return the Uri headers of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
+ * @return the Uri headers of this data source
+ */
+ public Map<String, String> getUriHeaders() {
+ if (mUriHeader == null) {
+ return null;
+ }
+ return new HashMap<String, String>(mUriHeader);
+ }
+
+ /**
+ * Return the Uri cookies of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
+ * @return the Uri cookies of this data source
+ */
+ public List<HttpCookie> getUriCookies() {
+ if (mUriCookies == null) {
+ return null;
+ }
+ return new ArrayList<HttpCookie>(mUriCookies);
+ }
+
+ /**
+ * Return the Context used for resolving the Uri of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
+ * @return the Context used for resolving the Uri of this data source
+ */
+ public Context getUriContext() {
+ return mUriContext;
+ }
+
+ /**
+ * Builder class for {@link DataSourceDesc} objects.
+ * <p> Here is an example where <code>Builder</code> is used to define the
+ * {@link DataSourceDesc} to be used by a {@link MediaPlayer2} instance:
+ *
+ * <pre class="prettyprint">
+ * DataSourceDesc oldDSD = mediaplayer2.getDataSourceDesc();
+ * DataSourceDesc newDSD = new DataSourceDesc.Builder(oldDSD)
+ * .setStartPosition(1000)
+ * .setEndPosition(15000)
+ * .build();
+ * mediaplayer2.setDataSourceDesc(newDSD);
+ * </pre>
+ */
+ public static class Builder {
+ private int mType = TYPE_NONE;
+
+ private Media2DataSource mMedia2DataSource;
+
+ private FileDescriptor mFD;
+ private long mFDOffset = 0;
+ private long mFDLength = LONG_MAX;
+
+ private Uri mUri;
+ private Map<String, String> mUriHeader;
+ private List<HttpCookie> mUriCookies;
+ private Context mUriContext;
+
+ private long mId = 0;
+ private long mStartPositionMs = 0;
+ private long mEndPositionMs = LONG_MAX;
+
+ /**
+ * Constructs a new Builder with the defaults.
+ */
+ public Builder() {
+ }
+
+ /**
+ * Constructs a new Builder from a given {@link DataSourceDesc} instance
+ * @param dsd the {@link DataSourceDesc} object whose data will be reused
+ * in the new Builder.
+ */
+ public Builder(DataSourceDesc dsd) {
+ mType = dsd.mType;
+ mMedia2DataSource = dsd.mMedia2DataSource;
+ mFD = dsd.mFD;
+ mFDOffset = dsd.mFDOffset;
+ mFDLength = dsd.mFDLength;
+ mUri = dsd.mUri;
+ mUriHeader = dsd.mUriHeader;
+ mUriCookies = dsd.mUriCookies;
+ mUriContext = dsd.mUriContext;
+
+ mId = dsd.mId;
+ mStartPositionMs = dsd.mStartPositionMs;
+ mEndPositionMs = dsd.mEndPositionMs;
+ }
+
+ /**
+ * Combines all of the fields that have been set and return a new
+ * {@link DataSourceDesc} object. <code>IllegalStateException</code> will be
+ * thrown if there is conflict between fields.
+ *
+ * @return a new {@link DataSourceDesc} object
+ */
+ public DataSourceDesc build() {
+ if (mType != TYPE_CALLBACK
+ && mType != TYPE_FD
+ && mType != TYPE_URI) {
+ throw new IllegalStateException("Illegal type: " + mType);
+ }
+ if (mStartPositionMs > mEndPositionMs) {
+ throw new IllegalStateException("Illegal start/end position: "
+ + mStartPositionMs + " : " + mEndPositionMs);
+ }
+
+ DataSourceDesc dsd = new DataSourceDesc();
+ dsd.mType = mType;
+ dsd.mMedia2DataSource = mMedia2DataSource;
+ dsd.mFD = mFD;
+ dsd.mFDOffset = mFDOffset;
+ dsd.mFDLength = mFDLength;
+ dsd.mUri = mUri;
+ dsd.mUriHeader = mUriHeader;
+ dsd.mUriCookies = mUriCookies;
+ dsd.mUriContext = mUriContext;
+
+ dsd.mId = mId;
+ dsd.mStartPositionMs = mStartPositionMs;
+ dsd.mEndPositionMs = mEndPositionMs;
+
+ return dsd;
+ }
+
+ /**
+ * Sets the Id of this data source.
+ *
+ * @param id the Id of this data source
+ * @return the same Builder instance.
+ */
+ public Builder setId(long id) {
+ mId = id;
+ return this;
+ }
+
+ /**
+ * Sets the start position in milliseconds at which the playback will start.
+ * Any negative number is treated as 0.
+ *
+ * @param position the start position in milliseconds at which the playback will start
+ * @return the same Builder instance.
+ *
+ */
+ public Builder setStartPosition(long position) {
+ if (position < 0) {
+ position = 0;
+ }
+ mStartPositionMs = position;
+ return this;
+ }
+
+ /**
+ * Sets the end position in milliseconds at which the playback will end.
+ * Any negative number is treated as maximum length of the data source.
+ *
+ * @param position the end position in milliseconds at which the playback will end
+ * @return the same Builder instance.
+ */
+ public Builder setEndPosition(long position) {
+ if (position < 0) {
+ position = LONG_MAX;
+ }
+ mEndPositionMs = position;
+ return this;
+ }
+
+ /**
+ * Sets the data source (Media2DataSource) to use.
+ *
+ * @param m2ds the Media2DataSource for the media you want to play
+ * @return the same Builder instance.
+ * @throws NullPointerException if m2ds is null.
+ */
+ public Builder setDataSource(Media2DataSource m2ds) {
+ Preconditions.checkNotNull(m2ds);
+ resetDataSource();
+ mType = TYPE_CALLBACK;
+ mMedia2DataSource = m2ds;
+ return this;
+ }
+
+ /**
+ * Sets the data source (FileDescriptor) to use. The FileDescriptor must be
+ * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility
+ * to close the file descriptor after the source has been used.
+ *
+ * @param fd the FileDescriptor for the file you want to play
+ * @return the same Builder instance.
+ * @throws NullPointerException if fd is null.
+ */
+ public Builder setDataSource(FileDescriptor fd) {
+ Preconditions.checkNotNull(fd);
+ resetDataSource();
+ mType = TYPE_FD;
+ mFD = fd;
+ return this;
+ }
+
+ /**
+ * Sets the data source (FileDescriptor) to use. The FileDescriptor must be
+ * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility
+ * to close the file descriptor after the source has been used.
+ *
+ * Any negative number for offset is treated as 0.
+ * Any negative number for length is treated as maximum length of the data source.
+ *
+ * @param fd the FileDescriptor for the file you want to play
+ * @param offset the offset into the file where the data to be played starts, in bytes
+ * @param length the length in bytes of the data to be played
+ * @return the same Builder instance.
+ * @throws NullPointerException if fd is null.
+ */
+ public Builder setDataSource(FileDescriptor fd, long offset, long length) {
+ Preconditions.checkNotNull(fd);
+ if (offset < 0) {
+ offset = 0;
+ }
+ if (length < 0) {
+ length = LONG_MAX;
+ }
+ resetDataSource();
+ mType = TYPE_FD;
+ mFD = fd;
+ mFDOffset = offset;
+ mFDLength = length;
+ return this;
+ }
+
+ /**
+ * Sets the data source as a content Uri.
+ *
+ * @param context the Context to use when resolving the Uri
+ * @param uri the Content URI of the data you want to play
+ * @return the same Builder instance.
+ * @throws NullPointerException if context or uri is null.
+ */
+ public Builder setDataSource(@NonNull Context context, @NonNull Uri uri) {
+ Preconditions.checkNotNull(context, "context cannot be null");
+ Preconditions.checkNotNull(uri, "uri cannot be null");
+ resetDataSource();
+ mType = TYPE_URI;
+ mUri = uri;
+ mUriContext = context;
+ return this;
+ }
+
+ /**
+ * Sets the data source as a content Uri.
+ *
+ * To provide cookies for the subsequent HTTP requests, you can install your own default
+ * cookie handler and use other variants of setDataSource APIs instead. Alternatively, you
+ * can use this API to pass the cookies as a list of HttpCookie. If the app has not
+ * installed a CookieHandler already, {@link MediaPlayer2} will create a CookieManager
+ * and populates its CookieStore with the provided cookies when this data source is passed
+ * to {@link MediaPlayer2}. If the app has installed its own handler already, the handler
+ * is required to be of CookieManager type such that {@link MediaPlayer2} can update the
+ * manager’s CookieStore.
+ *
+ * <p><strong>Note</strong> that the cross domain redirection is allowed by default,
+ * but that can be changed with key/value pairs through the headers parameter with
+ * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value to
+ * disallow or allow cross domain redirection.
+ *
+ * @param context the Context to use when resolving the Uri
+ * @param uri the Content URI of the data you want to play
+ * @param headers the headers to be sent together with the request for the data
+ * The headers must not include cookies. Instead, use the cookies param.
+ * @param cookies the cookies to be sent together with the request
+ * @return the same Builder instance.
+ * @throws NullPointerException if context or uri is null.
+ */
+ public Builder setDataSource(@NonNull Context context, @NonNull Uri uri,
+ @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies) {
+ Preconditions.checkNotNull(uri);
+ resetDataSource();
+ mType = TYPE_URI;
+ mUri = uri;
+ if (headers != null) {
+ mUriHeader = new HashMap<String, String>(headers);
+ }
+ if (cookies != null) {
+ mUriCookies = new ArrayList<HttpCookie>(cookies);
+ }
+ mUriContext = context;
+ return this;
+ }
+
+ private void resetDataSource() {
+ mType = TYPE_NONE;
+ mMedia2DataSource = null;
+ mFD = null;
+ mFDOffset = 0;
+ mFDLength = LONG_MAX;
+ mUri = null;
+ mUriHeader = null;
+ mUriCookies = null;
+ mUriContext = null;
+ }
+ }
+}
diff --git a/android/media/Media2DataSource.java b/android/media/Media2DataSource.java
new file mode 100644
index 0000000..8ee4a70
--- /dev/null
+++ b/android/media/Media2DataSource.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.media;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * For supplying media data to the framework. Implement this if your app has
+ * special requirements for the way media data is obtained.
+ *
+ * <p class="note">Methods of this interface may be called on multiple different
+ * threads. There will be a thread synchronization point between each call to ensure that
+ * modifications to the state of your Media2DataSource are visible to future calls. This means
+ * you don't need to do your own synchronization unless you're modifying the
+ * Media2DataSource from another thread while it's being used by the framework.</p>
+ *
+ */
+public abstract class Media2DataSource implements Closeable {
+ /**
+ * Called to request data from the given position.
+ *
+ * Implementations should should write up to {@code size} bytes into
+ * {@code buffer}, and return the number of bytes written.
+ *
+ * Return {@code 0} if size is zero (thus no bytes are read).
+ *
+ * Return {@code -1} to indicate that end of stream is reached.
+ *
+ * @param position the position in the data source to read from.
+ * @param buffer the buffer to read the data into.
+ * @param offset the offset within buffer to read the data into.
+ * @param size the number of bytes to read.
+ * @throws IOException on fatal errors.
+ * @return the number of bytes read, or -1 if there was an error.
+ */
+ public abstract int readAt(long position, byte[] buffer, int offset, int size)
+ throws IOException;
+
+ /**
+ * Called to get the size of the data source.
+ *
+ * @throws IOException on fatal errors
+ * @return the size of data source in bytes, or -1 if the size is unknown.
+ */
+ public abstract long getSize() throws IOException;
+}
diff --git a/android/media/Media2HTTPConnection.java b/android/media/Media2HTTPConnection.java
new file mode 100644
index 0000000..0d7825a
--- /dev/null
+++ b/android/media/Media2HTTPConnection.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.net.NetworkUtils;
+import android.os.StrictMode;
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.Proxy;
+import java.net.URL;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.NoRouteToHostException;
+import java.net.ProtocolException;
+import java.net.UnknownServiceException;
+import java.util.HashMap;
+import java.util.Map;
+
+import static android.media.MediaPlayer2.MEDIA_ERROR_UNSUPPORTED;
+
+/** @hide */
+public class Media2HTTPConnection {
+ private static final String TAG = "Media2HTTPConnection";
+ private static final boolean VERBOSE = false;
+
+ // connection timeout - 30 sec
+ private static final int CONNECT_TIMEOUT_MS = 30 * 1000;
+
+ private long mCurrentOffset = -1;
+ private URL mURL = null;
+ private Map<String, String> mHeaders = null;
+ private HttpURLConnection mConnection = null;
+ private long mTotalSize = -1;
+ private InputStream mInputStream = null;
+
+ private boolean mAllowCrossDomainRedirect = true;
+ private boolean mAllowCrossProtocolRedirect = true;
+
+ // from com.squareup.okhttp.internal.http
+ private final static int HTTP_TEMP_REDIRECT = 307;
+ private final static int MAX_REDIRECTS = 20;
+
+ public Media2HTTPConnection() {
+ CookieHandler cookieHandler = CookieHandler.getDefault();
+ if (cookieHandler == null) {
+ Log.w(TAG, "Media2HTTPConnection: Unexpected. No CookieHandler found.");
+ }
+ }
+
+ public boolean connect(String uri, String headers) {
+ if (VERBOSE) {
+ Log.d(TAG, "connect: uri=" + uri + ", headers=" + headers);
+ }
+
+ try {
+ disconnect();
+ mAllowCrossDomainRedirect = true;
+ mURL = new URL(uri);
+ mHeaders = convertHeaderStringToMap(headers);
+ } catch (MalformedURLException e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean parseBoolean(String val) {
+ try {
+ return Long.parseLong(val) != 0;
+ } catch (NumberFormatException e) {
+ return "true".equalsIgnoreCase(val) ||
+ "yes".equalsIgnoreCase(val);
+ }
+ }
+
+ /* returns true iff header is internal */
+ private boolean filterOutInternalHeaders(String key, String val) {
+ if ("android-allow-cross-domain-redirect".equalsIgnoreCase(key)) {
+ mAllowCrossDomainRedirect = parseBoolean(val);
+ // cross-protocol redirects are also controlled by this flag
+ mAllowCrossProtocolRedirect = mAllowCrossDomainRedirect;
+ } else {
+ return false;
+ }
+ return true;
+ }
+
+ private Map<String, String> convertHeaderStringToMap(String headers) {
+ HashMap<String, String> map = new HashMap<String, String>();
+
+ String[] pairs = headers.split("\r\n");
+ for (String pair : pairs) {
+ int colonPos = pair.indexOf(":");
+ if (colonPos >= 0) {
+ String key = pair.substring(0, colonPos);
+ String val = pair.substring(colonPos + 1);
+
+ if (!filterOutInternalHeaders(key, val)) {
+ map.put(key, val);
+ }
+ }
+ }
+
+ return map;
+ }
+
+ public void disconnect() {
+ teardownConnection();
+ mHeaders = null;
+ mURL = null;
+ }
+
+ private void teardownConnection() {
+ if (mConnection != null) {
+ if (mInputStream != null) {
+ try {
+ mInputStream.close();
+ } catch (IOException e) {
+ }
+ mInputStream = null;
+ }
+
+ mConnection.disconnect();
+ mConnection = null;
+
+ mCurrentOffset = -1;
+ }
+ }
+
+ private static final boolean isLocalHost(URL url) {
+ if (url == null) {
+ return false;
+ }
+
+ String host = url.getHost();
+
+ if (host == null) {
+ return false;
+ }
+
+ try {
+ if (host.equalsIgnoreCase("localhost")) {
+ return true;
+ }
+ if (NetworkUtils.numericToInetAddress(host).isLoopbackAddress()) {
+ return true;
+ }
+ } catch (IllegalArgumentException iex) {
+ }
+ return false;
+ }
+
+ private void seekTo(long offset) throws IOException {
+ teardownConnection();
+
+ try {
+ int response;
+ int redirectCount = 0;
+
+ URL url = mURL;
+
+ // do not use any proxy for localhost (127.0.0.1)
+ boolean noProxy = isLocalHost(url);
+
+ while (true) {
+ if (noProxy) {
+ mConnection = (HttpURLConnection)url.openConnection(Proxy.NO_PROXY);
+ } else {
+ mConnection = (HttpURLConnection)url.openConnection();
+ }
+ mConnection.setConnectTimeout(CONNECT_TIMEOUT_MS);
+
+ // handle redirects ourselves if we do not allow cross-domain redirect
+ mConnection.setInstanceFollowRedirects(mAllowCrossDomainRedirect);
+
+ if (mHeaders != null) {
+ for (Map.Entry<String, String> entry : mHeaders.entrySet()) {
+ mConnection.setRequestProperty(
+ entry.getKey(), entry.getValue());
+ }
+ }
+
+ if (offset > 0) {
+ mConnection.setRequestProperty(
+ "Range", "bytes=" + offset + "-");
+ }
+
+ response = mConnection.getResponseCode();
+ if (response != HttpURLConnection.HTTP_MULT_CHOICE &&
+ response != HttpURLConnection.HTTP_MOVED_PERM &&
+ response != HttpURLConnection.HTTP_MOVED_TEMP &&
+ response != HttpURLConnection.HTTP_SEE_OTHER &&
+ response != HTTP_TEMP_REDIRECT) {
+ // not a redirect, or redirect handled by HttpURLConnection
+ break;
+ }
+
+ if (++redirectCount > MAX_REDIRECTS) {
+ throw new NoRouteToHostException("Too many redirects: " + redirectCount);
+ }
+
+ String method = mConnection.getRequestMethod();
+ if (response == HTTP_TEMP_REDIRECT &&
+ !method.equals("GET") && !method.equals("HEAD")) {
+ // "If the 307 status code is received in response to a
+ // request other than GET or HEAD, the user agent MUST NOT
+ // automatically redirect the request"
+ throw new NoRouteToHostException("Invalid redirect");
+ }
+ String location = mConnection.getHeaderField("Location");
+ if (location == null) {
+ throw new NoRouteToHostException("Invalid redirect");
+ }
+ url = new URL(mURL /* TRICKY: don't use url! */, location);
+ if (!url.getProtocol().equals("https") &&
+ !url.getProtocol().equals("http")) {
+ throw new NoRouteToHostException("Unsupported protocol redirect");
+ }
+ boolean sameProtocol = mURL.getProtocol().equals(url.getProtocol());
+ if (!mAllowCrossProtocolRedirect && !sameProtocol) {
+ throw new NoRouteToHostException("Cross-protocol redirects are disallowed");
+ }
+ boolean sameHost = mURL.getHost().equals(url.getHost());
+ if (!mAllowCrossDomainRedirect && !sameHost) {
+ throw new NoRouteToHostException("Cross-domain redirects are disallowed");
+ }
+
+ if (response != HTTP_TEMP_REDIRECT) {
+ // update effective URL, unless it is a Temporary Redirect
+ mURL = url;
+ }
+ }
+
+ if (mAllowCrossDomainRedirect) {
+ // remember the current, potentially redirected URL if redirects
+ // were handled by HttpURLConnection
+ mURL = mConnection.getURL();
+ }
+
+ if (response == HttpURLConnection.HTTP_PARTIAL) {
+ // Partial content, we cannot just use getContentLength
+ // because what we want is not just the length of the range
+ // returned but the size of the full content if available.
+
+ String contentRange =
+ mConnection.getHeaderField("Content-Range");
+
+ mTotalSize = -1;
+ if (contentRange != null) {
+ // format is "bytes xxx-yyy/zzz
+ // where "zzz" is the total number of bytes of the
+ // content or '*' if unknown.
+
+ int lastSlashPos = contentRange.lastIndexOf('/');
+ if (lastSlashPos >= 0) {
+ String total =
+ contentRange.substring(lastSlashPos + 1);
+
+ try {
+ mTotalSize = Long.parseLong(total);
+ } catch (NumberFormatException e) {
+ }
+ }
+ }
+ } else if (response != HttpURLConnection.HTTP_OK) {
+ throw new IOException();
+ } else {
+ mTotalSize = mConnection.getContentLength();
+ }
+
+ if (offset > 0 && response != HttpURLConnection.HTTP_PARTIAL) {
+ // Some servers simply ignore "Range" requests and serve
+ // data from the start of the content.
+ throw new ProtocolException();
+ }
+
+ mInputStream =
+ new BufferedInputStream(mConnection.getInputStream());
+
+ mCurrentOffset = offset;
+ } catch (IOException e) {
+ mTotalSize = -1;
+ teardownConnection();
+ mCurrentOffset = -1;
+
+ throw e;
+ }
+ }
+
+ public int readAt(long offset, byte[] data, int size) {
+ StrictMode.ThreadPolicy policy =
+ new StrictMode.ThreadPolicy.Builder().permitAll().build();
+
+ StrictMode.setThreadPolicy(policy);
+
+ try {
+ if (offset != mCurrentOffset) {
+ seekTo(offset);
+ }
+
+ int n = mInputStream.read(data, 0, size);
+
+ if (n == -1) {
+ // InputStream signals EOS using a -1 result, our semantics
+ // are to return a 0-length read.
+ n = 0;
+ }
+
+ mCurrentOffset += n;
+
+ if (VERBOSE) {
+ Log.d(TAG, "readAt " + offset + " / " + size + " => " + n);
+ }
+
+ return n;
+ } catch (ProtocolException e) {
+ Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
+ return MEDIA_ERROR_UNSUPPORTED;
+ } catch (NoRouteToHostException e) {
+ Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
+ return MEDIA_ERROR_UNSUPPORTED;
+ } catch (UnknownServiceException e) {
+ Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
+ return MEDIA_ERROR_UNSUPPORTED;
+ } catch (IOException e) {
+ if (VERBOSE) {
+ Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
+ }
+ return -1;
+ } catch (Exception e) {
+ if (VERBOSE) {
+ Log.d(TAG, "unknown exception " + e);
+ Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
+ }
+ return -1;
+ }
+ }
+
+ public long getSize() {
+ if (mConnection == null) {
+ try {
+ seekTo(0);
+ } catch (IOException e) {
+ return -1;
+ }
+ }
+
+ return mTotalSize;
+ }
+
+ public String getMIMEType() {
+ if (mConnection == null) {
+ try {
+ seekTo(0);
+ } catch (IOException e) {
+ return "application/octet-stream";
+ }
+ }
+
+ return mConnection.getContentType();
+ }
+
+ public String getUri() {
+ return mURL.toString();
+ }
+}
diff --git a/android/media/Media2HTTPService.java b/android/media/Media2HTTPService.java
new file mode 100644
index 0000000..957acec
--- /dev/null
+++ b/android/media/Media2HTTPService.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.util.Log;
+
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.CookieStore;
+import java.net.HttpCookie;
+import java.util.List;
+
+/** @hide */
+public class Media2HTTPService {
+ private static final String TAG = "Media2HTTPService";
+ private List<HttpCookie> mCookies;
+ private Boolean mCookieStoreInitialized = new Boolean(false);
+
+ public Media2HTTPService(List<HttpCookie> cookies) {
+ mCookies = cookies;
+ Log.v(TAG, "Media2HTTPService(" + this + "): Cookies: " + cookies);
+ }
+
+ public Media2HTTPConnection makeHTTPConnection() {
+
+ synchronized (mCookieStoreInitialized) {
+ // Only need to do it once for all connections
+ if ( !mCookieStoreInitialized ) {
+ CookieHandler cookieHandler = CookieHandler.getDefault();
+ if (cookieHandler == null) {
+ cookieHandler = new CookieManager();
+ CookieHandler.setDefault(cookieHandler);
+ Log.v(TAG, "makeHTTPConnection: CookieManager created: " + cookieHandler);
+ } else {
+ Log.v(TAG, "makeHTTPConnection: CookieHandler (" + cookieHandler + ") exists.");
+ }
+
+ // Applying the bootstrapping cookies
+ if ( mCookies != null ) {
+ if ( cookieHandler instanceof CookieManager ) {
+ CookieManager cookieManager = (CookieManager)cookieHandler;
+ CookieStore store = cookieManager.getCookieStore();
+ for ( HttpCookie cookie : mCookies ) {
+ try {
+ store.add(null, cookie);
+ } catch ( Exception e ) {
+ Log.v(TAG, "makeHTTPConnection: CookieStore.add" + e);
+ }
+ //for extended debugging when needed
+ //Log.v(TAG, "MediaHTTPConnection adding Cookie[" + cookie.getName() +
+ // "]: " + cookie);
+ }
+ } else {
+ Log.w(TAG, "makeHTTPConnection: The installed CookieHandler is not a "
+ + "CookieManager. Can’t add the provided cookies to the cookie "
+ + "store.");
+ }
+ } // mCookies
+
+ mCookieStoreInitialized = true;
+
+ Log.v(TAG, "makeHTTPConnection(" + this + "): cookieHandler: " + cookieHandler +
+ " Cookies: " + mCookies);
+ } // mCookieStoreInitialized
+ } // synchronized
+
+ return new Media2HTTPConnection();
+ }
+
+ /* package private */ static Media2HTTPService createHTTPService(String path) {
+ return createHTTPService(path, null);
+ }
+
+ // when cookies are provided
+ static Media2HTTPService createHTTPService(String path, List<HttpCookie> cookies) {
+ if (path.startsWith("http://") || path.startsWith("https://")) {
+ return (new Media2HTTPService(cookies));
+ } else if (path.startsWith("widevine://")) {
+ Log.d(TAG, "Widevine classic is no longer supported");
+ }
+
+ return null;
+ }
+}
diff --git a/android/media/MediaBrowser2.java b/android/media/MediaBrowser2.java
new file mode 100644
index 0000000..be4be3f
--- /dev/null
+++ b/android/media/MediaBrowser2.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.update.ApiLoader;
+import android.media.update.MediaBrowser2Provider;
+import android.os.Bundle;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Browses media content offered by a {@link MediaLibraryService2}.
+ * @hide
+ */
+public class MediaBrowser2 extends MediaController2 {
+ // Equals to the ((MediaBrowser2Provider) getProvider())
+ private final MediaBrowser2Provider mProvider;
+
+ /**
+ * Callback to listen events from {@link MediaLibraryService2}.
+ */
+ public static class BrowserCallback extends MediaController2.ControllerCallback {
+ /**
+ * Called with the result of {@link #getBrowserRoot(Bundle)}.
+ * <p>
+ * {@code rootMediaId} and {@code rootExtra} can be {@code null} if the browser root isn't
+ * available.
+ *
+ * @param rootHints rootHints that you previously requested.
+ * @param rootMediaId media id of the browser root. Can be {@code null}
+ * @param rootExtra extra of the browser root. Can be {@code null}
+ */
+ public void onGetRootResult(Bundle rootHints, @Nullable String rootMediaId,
+ @Nullable Bundle rootExtra) { }
+
+ /**
+ * Called when the item has been returned by the library service for the previous
+ * {@link MediaBrowser2#getItem} call.
+ * <p>
+ * Result can be null if there had been error.
+ *
+ * @param mediaId media id
+ * @param result result. Can be {@code null}
+ */
+ public void onItemLoaded(@NonNull String mediaId, @Nullable MediaItem2 result) { }
+
+ /**
+ * Called when the list of items has been returned by the library service for the previous
+ * {@link MediaBrowser2#getChildren(String, int, int, Bundle)}.
+ *
+ * @param parentId parent id
+ * @param page page number that you've specified
+ * @param pageSize page size that you've specified
+ * @param options optional bundle that you've specified
+ * @param result result. Can be {@code null}
+ */
+ public void onChildrenLoaded(@NonNull String parentId, int page, int pageSize,
+ @Nullable Bundle options, @Nullable List<MediaItem2> result) { }
+
+ /**
+ * Called when there's change in the parent's children.
+ *
+ * @param parentId parent id that you've specified with subscribe
+ * @param options optional bundle that you've specified with subscribe
+ */
+ public void onChildrenChanged(@NonNull String parentId, @Nullable Bundle options) { }
+
+ /**
+ * Called when the search result has been returned by the library service for the previous
+ * {@link MediaBrowser2#search(String, int, int, Bundle)}.
+ * <p>
+ * Result can be null if there had been error.
+ *
+ * @param query query string that you've specified
+ * @param page page number that you've specified
+ * @param pageSize page size that you've specified
+ * @param options optional bundle that you've specified
+ * @param result result. Can be {@code null}
+ */
+ public void onSearchResult(@NonNull String query, int page, int pageSize,
+ @Nullable Bundle options, @Nullable List<MediaItem2> result) { }
+ }
+
+ public MediaBrowser2(Context context, SessionToken2 token, BrowserCallback callback,
+ Executor executor) {
+ super(context, token, callback, executor);
+ mProvider = (MediaBrowser2Provider) getProvider();
+ }
+
+ @Override
+ MediaBrowser2Provider createProvider(Context context, SessionToken2 token,
+ ControllerCallback callback, Executor executor) {
+ return ApiLoader.getProvider(context)
+ .createMediaBrowser2(this, context, token, (BrowserCallback) callback, executor);
+ }
+
+ public void getBrowserRoot(Bundle rootHints) {
+ mProvider.getBrowserRoot_impl(rootHints);
+ }
+
+ /**
+ * Subscribe to a parent id for the change in its children. When there's a change,
+ * {@link BrowserCallback#onChildrenChanged(String, Bundle)} will be called with the bundle
+ * that you've specified. You should call {@link #getChildren(String, int, int, Bundle)} to get
+ * the actual contents for the parent.
+ *
+ * @param parentId parent id
+ * @param options optional bundle
+ */
+ public void subscribe(String parentId, @Nullable Bundle options) {
+ mProvider.subscribe_impl(parentId, options);
+ }
+
+ /**
+ * Unsubscribe for changes to the children of the parent, which was previously subscribed with
+ * {@link #subscribe(String, Bundle)}.
+ *
+ * @param parentId parent id
+ * @param options optional bundle
+ */
+ public void unsubscribe(String parentId, @Nullable Bundle options) {
+ mProvider.unsubscribe_impl(parentId, options);
+ }
+
+ /**
+ * Get the media item with the given media id. Result would be sent back asynchronously with the
+ * {@link BrowserCallback#onItemLoaded(String, MediaItem2)}.
+ *
+ * @param mediaId media id
+ */
+ public void getItem(String mediaId) {
+ mProvider.getItem_impl(mediaId);
+ }
+
+ /**
+ * Get list of children under the parent. Result would be sent back asynchronously with the
+ * {@link BrowserCallback#onChildrenLoaded(String, int, int, Bundle, List)}.
+ *
+ * @param parentId
+ * @param page
+ * @param pageSize
+ * @param options
+ */
+ public void getChildren(String parentId, int page, int pageSize, @Nullable Bundle options) {
+ mProvider.getChildren_impl(parentId, page, pageSize, options);
+ }
+
+ /**
+ *
+ * @param query search query deliminated by string
+ * @param page page number to get search result. Starts from {@code 1}
+ * @param pageSize page size. Should be greater or equal to {@code 1}
+ * @param extras extra bundle
+ */
+ public void search(String query, int page, int pageSize, Bundle extras) {
+ mProvider.search_impl(query, page, pageSize, extras);
+ }
+}
diff --git a/android/media/MediaBrowser2Test.java b/android/media/MediaBrowser2Test.java
new file mode 100644
index 0000000..5c960c8
--- /dev/null
+++ b/android/media/MediaBrowser2Test.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.media.MediaBrowser2.BrowserCallback;
+import android.media.MediaSession2.CommandGroup;
+import android.os.Bundle;
+import android.support.annotation.CallSuper;
+import android.support.annotation.NonNull;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests {@link MediaBrowser2}.
+ * <p>
+ * This test inherits {@link MediaController2Test} to ensure that inherited APIs from
+ * {@link MediaController2} works cleanly.
+ */
+// TODO(jaewan): Implement host-side test so browser and service can run in different processes.
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MediaBrowser2Test extends MediaController2Test {
+ private static final String TAG = "MediaBrowser2Test";
+
+ @Override
+ TestControllerInterface onCreateController(@NonNull SessionToken2 token,
+ @NonNull TestControllerCallbackInterface callback) {
+ return new TestMediaBrowser(mContext, token, new TestBrowserCallback(callback));
+ }
+
+ @Test
+ public void testGetBrowserRoot() throws InterruptedException {
+ final Bundle param = new Bundle();
+ param.putString(TAG, TAG);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+ @Override
+ public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {
+ assertTrue(TestUtils.equals(param, rootHints));
+ assertEquals(MockMediaLibraryService2.ROOT_ID, rootMediaId);
+ assertTrue(TestUtils.equals(MockMediaLibraryService2.EXTRA, rootExtra));
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser =
+ (MediaBrowser2) createController(token,true, callback);
+ browser.getBrowserRoot(param);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ public static class TestBrowserCallback extends BrowserCallback
+ implements WaitForConnectionInterface {
+ private final TestControllerCallbackInterface mCallbackProxy;
+ public final CountDownLatch connectLatch = new CountDownLatch(1);
+ public final CountDownLatch disconnectLatch = new CountDownLatch(1);
+
+ TestBrowserCallback(TestControllerCallbackInterface callbackProxy) {
+ mCallbackProxy = callbackProxy;
+ }
+
+ @CallSuper
+ @Override
+ public void onConnected(CommandGroup commands) {
+ super.onConnected(commands);
+ connectLatch.countDown();
+ }
+
+ @CallSuper
+ @Override
+ public void onDisconnected() {
+ super.onDisconnected();
+ disconnectLatch.countDown();
+ }
+
+ @Override
+ public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {
+ mCallbackProxy.onGetRootResult(rootHints, rootMediaId, rootExtra);
+ }
+
+ @Override
+ public void waitForConnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Override
+ public void waitForDisconnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+ }
+
+ public class TestMediaBrowser extends MediaBrowser2 implements TestControllerInterface {
+ private final BrowserCallback mCallback;
+
+ public TestMediaBrowser(@NonNull Context context, @NonNull SessionToken2 token,
+ @NonNull ControllerCallback callback) {
+ super(context, token, (BrowserCallback) callback, sHandlerExecutor);
+ mCallback = (BrowserCallback) callback;
+ }
+
+ @Override
+ public BrowserCallback getCallback() {
+ return mCallback;
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/media/MediaCodecInfo.java b/android/media/MediaCodecInfo.java
index f41e33f..44d9099 100644
--- a/android/media/MediaCodecInfo.java
+++ b/android/media/MediaCodecInfo.java
@@ -2639,7 +2639,8 @@
/**
* Returns the supported range of quality values.
*
- * @hide
+ * Quality is implementation-specific. As a general rule, a higher quality
+ * setting results in a better image quality and a lower compression ratio.
*/
public Range<Integer> getQualityRange() {
return mQualityRange;
@@ -2751,7 +2752,7 @@
}
if (info.containsKey("feature-bitrate-modes")) {
for (String mode: info.getString("feature-bitrate-modes").split(",")) {
- mBitControl |= parseBitrateMode(mode);
+ mBitControl |= (1 << parseBitrateMode(mode));
}
}
diff --git a/android/media/MediaController2.java b/android/media/MediaController2.java
new file mode 100644
index 0000000..d669bc1
--- /dev/null
+++ b/android/media/MediaController2.java
@@ -0,0 +1,616 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.media.MediaSession2.Command;
+import android.media.MediaSession2.CommandButton;
+import android.media.MediaSession2.CommandGroup;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.PlaylistParam;
+import android.media.session.MediaSessionManager;
+import android.media.update.ApiLoader;
+import android.media.update.MediaController2Provider;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Allows an app to interact with an active {@link MediaSession2} or a
+ * {@link MediaSessionService2} in any status. Media buttons and other commands can be sent to
+ * the session.
+ * <p>
+ * When you're done, use {@link #close()} to clean up resources. This also helps session service
+ * to be destroyed when there's no controller associated with it.
+ * <p>
+ * When controlling {@link MediaSession2}, the controller will be available immediately after
+ * the creation.
+ * <p>
+ * When controlling {@link MediaSessionService2}, the {@link MediaController2} would be
+ * available only if the session service allows this controller by
+ * {@link MediaSession2.SessionCallback#onConnect(ControllerInfo)} for the service. Wait
+ * {@link ControllerCallback#onConnected(CommandGroup)} or
+ * {@link ControllerCallback#onDisconnected()} for the result.
+ * <p>
+ * A controller can be created through token from {@link MediaSessionManager} if you hold the
+ * signature|privileged permission "android.permission.MEDIA_CONTENT_CONTROL" permission or are
+ * an enabled notification listener or by getting a {@link SessionToken2} directly the
+ * the session owner.
+ * <p>
+ * MediaController2 objects are thread-safe.
+ * <p>
+ * @see MediaSession2
+ * @see MediaSessionService2
+ * @hide
+ */
+// TODO(jaewan): Unhide
+// TODO(jaewan): Revisit comments. Currently MediaBrowser case is missing.
+public class MediaController2 implements AutoCloseable {
+ /**
+ * Interface for listening to change in activeness of the {@link MediaSession2}. It's
+ * active if and only if it has set a player.
+ */
+ public abstract static class ControllerCallback {
+ /**
+ * Called when the controller is successfully connected to the session. The controller
+ * becomes available afterwards.
+ *
+ * @param allowedCommands commands that's allowed by the session.
+ */
+ public void onConnected(CommandGroup allowedCommands) { }
+
+ /**
+ * Called when the session refuses the controller or the controller is disconnected from
+ * the session. The controller becomes unavailable afterwards and the callback wouldn't
+ * be called.
+ * <p>
+ * It will be also called after the {@link #close()}, so you can put clean up code here.
+ * You don't need to call {@link #close()} after this.
+ */
+ public void onDisconnected() { }
+
+ /**
+ * Called when the session set the custom layout through the
+ * {@link MediaSession2#setCustomLayout(ControllerInfo, List)}.
+ * <p>
+ * Can be called before {@link #onConnected(CommandGroup)} is called.
+ *
+ * @param layout
+ */
+ public void onCustomLayoutChanged(List<CommandButton> layout) { }
+
+ /**
+ * Called when the session has changed anything related with the {@link PlaybackInfo}.
+ *
+ * @param info new playback info
+ */
+ public void onAudioInfoChanged(PlaybackInfo info) { }
+
+ /**
+ * Called when the allowed commands are changed by session.
+ *
+ * @param commands newly allowed commands
+ */
+ public void onAllowedCommandsChanged(CommandGroup commands) { }
+
+ /**
+ * Called when the session sent a custom command.
+ *
+ * @param command
+ * @param args
+ * @param receiver
+ */
+ public void onCustomCommand(Command command, @Nullable Bundle args,
+ @Nullable ResultReceiver receiver) { }
+
+ /**
+ * Called when the playlist is changed.
+ *
+ * @param list
+ * @param param
+ */
+ public void onPlaylistChanged(
+ @NonNull List<MediaItem2> list, @NonNull PlaylistParam param) { }
+
+ /**
+ * Called when the playback state is changed.
+ *
+ * @param state
+ */
+ public void onPlaybackStateChanged(@NonNull PlaybackState2 state) { }
+ }
+
+ /**
+ * Holds information about the current playback and how audio is handled for
+ * this session.
+ */
+ // The same as MediaController.PlaybackInfo
+ public static final class PlaybackInfo {
+ /**
+ * The session uses remote playback.
+ */
+ public static final int PLAYBACK_TYPE_REMOTE = 2;
+ /**
+ * The session uses local playback.
+ */
+ public static final int PLAYBACK_TYPE_LOCAL = 1;
+
+ private final int mVolumeType;
+ private final int mVolumeControl;
+ private final int mMaxVolume;
+ private final int mCurrentVolume;
+ private final AudioAttributes mAudioAttrs;
+
+ /**
+ * @hide
+ */
+ public PlaybackInfo(int type, AudioAttributes attrs, int control, int max, int current) {
+ mVolumeType = type;
+ mAudioAttrs = attrs;
+ mVolumeControl = control;
+ mMaxVolume = max;
+ mCurrentVolume = current;
+ }
+
+ /**
+ * Get the type of playback which affects volume handling. One of:
+ * <ul>
+ * <li>{@link #PLAYBACK_TYPE_LOCAL}</li>
+ * <li>{@link #PLAYBACK_TYPE_REMOTE}</li>
+ * </ul>
+ *
+ * @return The type of playback this session is using.
+ */
+ public int getPlaybackType() {
+ return mVolumeType;
+ }
+
+ /**
+ * Get the audio attributes for this session. The attributes will affect
+ * volume handling for the session. When the volume type is
+ * {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} these may be ignored by the
+ * remote volume handler.
+ *
+ * @return The attributes for this session.
+ */
+ public AudioAttributes getAudioAttributes() {
+ return mAudioAttrs;
+ }
+
+ /**
+ * Get the type of volume control that can be used. One of:
+ * <ul>
+ * <li>{@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}</li>
+ * <li>{@link VolumeProvider#VOLUME_CONTROL_RELATIVE}</li>
+ * <li>{@link VolumeProvider#VOLUME_CONTROL_FIXED}</li>
+ * </ul>
+ *
+ * @return The type of volume control that may be used with this
+ * session.
+ */
+ public int getVolumeControl() {
+ return mVolumeControl;
+ }
+
+ /**
+ * Get the maximum volume that may be set for this session.
+ *
+ * @return The maximum allowed volume where this session is playing.
+ */
+ public int getMaxVolume() {
+ return mMaxVolume;
+ }
+
+ /**
+ * Get the current volume for this session.
+ *
+ * @return The current volume where this session is playing.
+ */
+ public int getCurrentVolume() {
+ return mCurrentVolume;
+ }
+ }
+
+ private final MediaController2Provider mProvider;
+
+ /**
+ * Create a {@link MediaController2} from the {@link SessionToken2}. This connects to the session
+ * and may wake up the service if it's not available.
+ *
+ * @param context Context
+ * @param token token to connect to
+ * @param callback controller callback to receive changes in
+ * @param executor executor to run callbacks on.
+ */
+ // TODO(jaewan): Put @CallbackExecutor to the constructor.
+ public MediaController2(@NonNull Context context, @NonNull SessionToken2 token,
+ @NonNull ControllerCallback callback, @NonNull Executor executor) {
+ super();
+
+ // This also connects to the token.
+ // Explicit connect() isn't added on purpose because retrying connect() is impossible with
+ // session whose session binder is only valid while it's active.
+ // prevent a controller from reusable after the
+ // session is released and recreated.
+ mProvider = createProvider(context, token, callback, executor);
+ }
+
+ MediaController2Provider createProvider(@NonNull Context context,
+ @NonNull SessionToken2 token, @NonNull ControllerCallback callback,
+ @NonNull Executor executor) {
+ return ApiLoader.getProvider(context)
+ .createMediaController2(this, context, token, callback, executor);
+ }
+
+ /**
+ * Release this object, and disconnect from the session. After this, callbacks wouldn't be
+ * received.
+ */
+ @Override
+ public void close() {
+ mProvider.close_impl();
+ }
+
+ /**
+ * @hide
+ */
+ public MediaController2Provider getProvider() {
+ return mProvider;
+ }
+
+ /**
+ * @return token
+ */
+ public @NonNull
+ SessionToken2 getSessionToken() {
+ return mProvider.getSessionToken_impl();
+ }
+
+ /**
+ * Returns whether this class is connected to active {@link MediaSession2} or not.
+ */
+ public boolean isConnected() {
+ return mProvider.isConnected_impl();
+ }
+
+ public void play() {
+ mProvider.play_impl();
+ }
+
+ public void pause() {
+ mProvider.pause_impl();
+ }
+
+ public void stop() {
+ mProvider.stop_impl();
+ }
+
+ public void skipToPrevious() {
+ mProvider.skipToPrevious_impl();
+ }
+
+ public void skipToNext() {
+ mProvider.skipToNext_impl();
+ }
+
+ /**
+ * Request that the player prepare its playback. In other words, other sessions can continue
+ * to play during the preparation of this session. This method can be used to speed up the
+ * start of the playback. Once the preparation is done, the session will change its playback
+ * state to {@link PlaybackState2#STATE_PAUSED}. Afterwards, {@link #play} can be called to
+ * start playback.
+ */
+ public void prepare() {
+ mProvider.prepare_impl();
+ }
+
+ /**
+ * Start fast forwarding. If playback is already fast forwarding this
+ * may increase the rate.
+ */
+ public void fastForward() {
+ mProvider.fastForward_impl();
+ }
+
+ /**
+ * Start rewinding. If playback is already rewinding this may increase
+ * the rate.
+ */
+ public void rewind() {
+ mProvider.rewind_impl();
+ }
+
+ /**
+ * Move to a new location in the media stream.
+ *
+ * @param pos Position to move to, in milliseconds.
+ */
+ public void seekTo(long pos) {
+ mProvider.seekTo_impl(pos);
+ }
+
+ /**
+ * Sets the index of current DataSourceDesc in the play list to be played.
+ *
+ * @param index the index of DataSourceDesc in the play list you want to play
+ * @throws IllegalArgumentException if the play list is null
+ * @throws NullPointerException if index is outside play list range
+ */
+ public void setCurrentPlaylistItem(int index) {
+ mProvider.setCurrentPlaylistItem_impl(index);
+ }
+
+ /**
+ * @hide
+ */
+ public void skipForward() {
+ // To match with KEYCODE_MEDIA_SKIP_FORWARD
+ }
+
+ /**
+ * @hide
+ */
+ public void skipBackward() {
+ // To match with KEYCODE_MEDIA_SKIP_BACKWARD
+ }
+
+ /**
+ * Request that the player start playback for a specific media id.
+ *
+ * @param mediaId The id of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be played.
+ */
+ public void playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+ mProvider.playFromMediaId_impl(mediaId, extras);
+ }
+
+ /**
+ * Request that the player start playback for a specific search query.
+ * An empty or null query should be treated as a request to play any
+ * music.
+ *
+ * @param query The search query.
+ * @param extras Optional extras that can include extra information
+ * about the query.
+ */
+ public void playFromSearch(@NonNull String query, @Nullable Bundle extras) {
+ mProvider.playFromSearch_impl(query, extras);
+ }
+
+ /**
+ * Request that the player start playback for a specific {@link Uri}.
+ *
+ * @param uri The URI of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be played.
+ */
+ public void playFromUri(@NonNull String uri, @Nullable Bundle extras) {
+ mProvider.playFromUri_impl(uri, extras);
+ }
+
+
+ /**
+ * Request that the player prepare playback for a specific media id. In other words, other
+ * sessions can continue to play during the preparation of this session. This method can be
+ * used to speed up the start of the playback. Once the preparation is done, the session
+ * will change its playback state to {@link PlaybackState2#STATE_PAUSED}. Afterwards,
+ * {@link #play} can be called to start playback. If the preparation is not needed,
+ * {@link #playFromMediaId} can be directly called without this method.
+ *
+ * @param mediaId The id of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be prepared.
+ */
+ public void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+ mProvider.prepareMediaId_impl(mediaId, extras);
+ }
+
+ /**
+ * Request that the player prepare playback for a specific search query. An empty or null
+ * query should be treated as a request to prepare any music. In other words, other sessions
+ * can continue to play during the preparation of this session. This method can be used to
+ * speed up the start of the playback. Once the preparation is done, the session will
+ * change its playback state to {@link PlaybackState2#STATE_PAUSED}. Afterwards,
+ * {@link #play} can be called to start playback. If the preparation is not needed,
+ * {@link #playFromSearch} can be directly called without this method.
+ *
+ * @param query The search query.
+ * @param extras Optional extras that can include extra information
+ * about the query.
+ */
+ public void prepareFromSearch(@NonNull String query, @Nullable Bundle extras) {
+ mProvider.prepareFromSearch_impl(query, extras);
+ }
+
+ /**
+ * Request that the player prepare playback for a specific {@link Uri}. In other words,
+ * other sessions can continue to play during the preparation of this session. This method
+ * can be used to speed up the start of the playback. Once the preparation is done, the
+ * session will change its playback state to {@link PlaybackState2#STATE_PAUSED}. Afterwards,
+ * {@link #play} can be called to start playback. If the preparation is not needed,
+ * {@link #playFromUri} can be directly called without this method.
+ *
+ * @param uri The URI of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be prepared.
+ */
+ public void prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
+ mProvider.prepareFromUri_impl(uri, extras);
+ }
+
+ /**
+ * Set the volume of the output this session is playing on. The command will be ignored if it
+ * does not support {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}.
+ * <p>
+ * If the session is local playback, this changes the device's volume with the stream that
+ * session's player is using. Flags will be specified for the {@link AudioManager}.
+ * <p>
+ * If the session is remote player (i.e. session has set volume provider), its volume provider
+ * will receive this request instead.
+ *
+ * @see #getPlaybackInfo()
+ * @param value The value to set it to, between 0 and the reported max.
+ * @param flags flags from {@link AudioManager} to include with the volume request for local
+ * playback
+ */
+ public void setVolumeTo(int value, int flags) {
+ mProvider.setVolumeTo_impl(value, flags);
+ }
+
+ /**
+ * Adjust the volume of the output this session is playing on. The direction
+ * must be one of {@link AudioManager#ADJUST_LOWER},
+ * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
+ * The command will be ignored if the session does not support
+ * {@link VolumeProvider#VOLUME_CONTROL_RELATIVE} or
+ * {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}.
+ * <p>
+ * If the session is local playback, this changes the device's volume with the stream that
+ * session's player is using. Flags will be specified for the {@link AudioManager}.
+ * <p>
+ * If the session is remote player (i.e. session has set volume provider), its volume provider
+ * will receive this request instead.
+ *
+ * @see #getPlaybackInfo()
+ * @param direction The direction to adjust the volume in.
+ * @param flags flags from {@link AudioManager} to include with the volume request for local
+ * playback
+ */
+ public void adjustVolume(int direction, int flags) {
+ mProvider.adjustVolume_impl(direction, flags);
+ }
+
+ /**
+ * Get the rating type supported by the session. One of:
+ * <ul>
+ * <li>{@link Rating2#RATING_NONE}</li>
+ * <li>{@link Rating2#RATING_HEART}</li>
+ * <li>{@link Rating2#RATING_THUMB_UP_DOWN}</li>
+ * <li>{@link Rating2#RATING_3_STARS}</li>
+ * <li>{@link Rating2#RATING_4_STARS}</li>
+ * <li>{@link Rating2#RATING_5_STARS}</li>
+ * <li>{@link Rating2#RATING_PERCENTAGE}</li>
+ * </ul>
+ *
+ * @return The supported rating type
+ */
+ public int getRatingType() {
+ return mProvider.getRatingType_impl();
+ }
+
+ /**
+ * Get an intent for launching UI associated with this session if one exists.
+ *
+ * @return A {@link PendingIntent} to launch UI or null.
+ */
+ public @Nullable PendingIntent getSessionActivity() {
+ return mProvider.getSessionActivity_impl();
+ }
+
+ /**
+ * Get the latest {@link PlaybackState2} from the session.
+ *
+ * @return a playback state
+ */
+ public PlaybackState2 getPlaybackState() {
+ return mProvider.getPlaybackState_impl();
+ }
+
+ /**
+ * Get the current playback info for this session.
+ *
+ * @return The current playback info or null.
+ */
+ public @Nullable PlaybackInfo getPlaybackInfo() {
+ return mProvider.getPlaybackInfo_impl();
+ }
+
+ /**
+ * Rate the current content. This will cause the rating to be set for
+ * the current user. The Rating type must match the type returned by
+ * {@link #getRatingType()}.
+ *
+ * @param rating The rating to set for the current content
+ */
+ public void setRating(Rating2 rating) {
+ mProvider.setRating_impl(rating);
+ }
+
+ /**
+ * Send custom command to the session
+ *
+ * @param command custom command
+ * @param args optional argument
+ * @param cb optional result receiver
+ */
+ public void sendCustomCommand(@NonNull Command command, @Nullable Bundle args,
+ @Nullable ResultReceiver cb) {
+ mProvider.sendCustomCommand_impl(command, args, cb);
+ }
+
+ /**
+ * Return playlist from the session.
+ *
+ * @return playlist. Can be {@code null} if the controller doesn't have enough permission.
+ */
+ public @Nullable List<MediaItem2> getPlaylist() {
+ return mProvider.getPlaylist_impl();
+ }
+
+ public @Nullable PlaylistParam getPlaylistParam() {
+ return mProvider.getPlaylistParam_impl();
+ }
+
+ /**
+ * Removes the media item at index in the play list.
+ *<p>
+ * If index is same as the current index of the playlist, current playback
+ * will be stopped and playback moves to next source in the list.
+ *
+ * @return the removed DataSourceDesc at index in the play list
+ * @throws IllegalArgumentException if the play list is null
+ * @throws IndexOutOfBoundsException if index is outside play list range
+ */
+ // TODO(jaewan): Remove with index was previously rejected by council (b/36524925)
+ // TODO(jaewan): Should we also add movePlaylistItem from index to index?
+ public void removePlaylistItem(MediaItem2 item) {
+ mProvider.removePlaylistItem_impl(item);
+ }
+
+ /**
+ * Inserts the media item to the play list at position index.
+ * <p>
+ * This will not change the currently playing media item.
+ * If index is less than or equal to the current index of the play list,
+ * the current index of the play list will be incremented correspondingly.
+ *
+ * @param index the index you want to add dsd to the play list
+ * @param item the media item you want to add to the play list
+ * @throws IndexOutOfBoundsException if index is outside play list range
+ * @throws NullPointerException if dsd is null
+ */
+ public void addPlaylistItem(int index, MediaItem2 item) {
+ mProvider.addPlaylistItem_impl(index, item);
+ }
+}
diff --git a/android/media/MediaController2Test.java b/android/media/MediaController2Test.java
new file mode 100644
index 0000000..ae67a95
--- /dev/null
+++ b/android/media/MediaController2Test.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.TestUtils.SyncHandler;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static android.media.TestUtils.createPlaybackState;
+import static org.junit.Assert.*;
+
+/**
+ * Tests {@link MediaController2}.
+ */
+// TODO(jaewan): Implement host-side test so controller and session can run in different processes.
+// TODO(jaewan): Fix flaky failure -- see MediaController2Impl.getController()
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@FlakyTest
+public class MediaController2Test extends MediaSession2TestBase {
+ private static final String TAG = "MediaController2Test";
+
+ MediaSession2 mSession;
+ MediaController2 mController;
+ MockPlayer mPlayer;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ // Create this test specific MediaSession2 to use our own Handler.
+ sHandler.postAndSync(()->{
+ mPlayer = new MockPlayer(1);
+ mSession = new MediaSession2.Builder(mContext, mPlayer).setId(TAG).build();
+ });
+
+ mController = createController(mSession.getToken());
+ TestServiceRegistry.getInstance().setHandler(sHandler);
+ }
+
+ @After
+ @Override
+ public void cleanUp() throws Exception {
+ super.cleanUp();
+ sHandler.postAndSync(() -> {
+ if (mSession != null) {
+ mSession.close();
+ }
+ });
+ TestServiceRegistry.getInstance().cleanUp();
+ }
+
+ @Test
+ public void testPlay() throws InterruptedException {
+ mController.play();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mPlayCalled);
+ }
+
+ @Test
+ public void testPause() throws InterruptedException {
+ mController.pause();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mPauseCalled);
+ }
+
+
+ @Test
+ public void testSkipToPrevious() throws InterruptedException {
+ mController.skipToPrevious();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mSkipToPreviousCalled);
+ }
+
+ @Test
+ public void testSkipToNext() throws InterruptedException {
+ mController.skipToNext();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mSkipToNextCalled);
+ }
+
+ @Test
+ public void testStop() throws InterruptedException {
+ mController.stop();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mStopCalled);
+ }
+
+ @Test
+ public void testGetPackageName() {
+ assertEquals(mContext.getPackageName(), mController.getSessionToken().getPackageName());
+ }
+
+ @Test
+ public void testGetPlaybackState() throws InterruptedException {
+ // TODO(jaewan): add equivalent test later
+ /*
+ final CountDownLatch latch = new CountDownLatch(1);
+ final MediaPlayerBase.PlaybackListener listener = (state) -> {
+ assertEquals(PlaybackState.STATE_BUFFERING, state.getState());
+ latch.countDown();
+ };
+ assertNull(mController.getPlaybackState());
+ mController.addPlaybackListener(listener, sHandler);
+
+ mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_BUFFERING));
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertEquals(PlaybackState.STATE_BUFFERING, mController.getPlaybackState().getState());
+ */
+ }
+
+ // TODO(jaewan): add equivalent test later
+ /*
+ @Test
+ public void testAddPlaybackListener() throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(2);
+ final MediaPlayerBase.PlaybackListener listener = (state) -> {
+ switch ((int) latch.getCount()) {
+ case 2:
+ assertEquals(PlaybackState.STATE_PLAYING, state.getState());
+ break;
+ case 1:
+ assertEquals(PlaybackState.STATE_PAUSED, state.getState());
+ break;
+ }
+ latch.countDown();
+ };
+
+ mController.addPlaybackListener(listener, sHandler);
+ sHandler.postAndSync(()->{
+ mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
+ mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
+ });
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testRemovePlaybackListener() throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final MediaPlayerBase.PlaybackListener listener = (state) -> {
+ fail();
+ latch.countDown();
+ };
+ mController.addPlaybackListener(listener, sHandler);
+ mController.removePlaybackListener(listener);
+ mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
+ assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ */
+
+ @Test
+ public void testControllerCallback_onConnected() throws InterruptedException {
+ // createController() uses controller callback to wait until the controller becomes
+ // available.
+ MediaController2 controller = createController(mSession.getToken());
+ assertNotNull(controller);
+ }
+
+ @Test
+ public void testControllerCallback_sessionRejects() throws InterruptedException {
+ final MediaSession2.SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
+ return null;
+ }
+ };
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext, mPlayer)
+ .setSessionCallback(sHandlerExecutor, sessionCallback).build();
+ });
+ MediaController2 controller =
+ createController(mSession.getToken(), false, null);
+ assertNotNull(controller);
+ waitForConnect(controller, false);
+ waitForDisconnect(controller, true);
+ }
+
+ @Test
+ public void testControllerCallback_releaseSession() throws InterruptedException {
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ });
+ waitForDisconnect(mController, true);
+ }
+
+ @Test
+ public void testControllerCallback_release() throws InterruptedException {
+ mController.close();
+ waitForDisconnect(mController, true);
+ }
+
+ @Test
+ public void testIsConnected() throws InterruptedException {
+ assertTrue(mController.isConnected());
+ sHandler.postAndSync(()->{
+ mSession.close();
+ });
+ // postAndSync() to wait until the disconnection is propagated.
+ sHandler.postAndSync(()->{
+ assertFalse(mController.isConnected());
+ });
+ }
+
+ /**
+ * Test potential deadlock for calls between controller and session.
+ */
+ @Test
+ public void testDeadlock() throws InterruptedException {
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ mSession = null;
+ });
+
+ // Two more threads are needed not to block test thread nor test wide thread (sHandler).
+ final HandlerThread sessionThread = new HandlerThread("testDeadlock_session");
+ final HandlerThread testThread = new HandlerThread("testDeadlock_test");
+ sessionThread.start();
+ testThread.start();
+ final SyncHandler sessionHandler = new SyncHandler(sessionThread.getLooper());
+ final Handler testHandler = new Handler(testThread.getLooper());
+ final CountDownLatch latch = new CountDownLatch(1);
+ try {
+ final MockPlayer player = new MockPlayer(0);
+ sessionHandler.postAndSync(() -> {
+ mSession = new MediaSession2.Builder(mContext, mPlayer)
+ .setId("testDeadlock").build();
+ });
+ final MediaController2 controller = createController(mSession.getToken());
+ testHandler.post(() -> {
+ final PlaybackState2 state = createPlaybackState(PlaybackState.STATE_ERROR);
+ for (int i = 0; i < 100; i++) {
+ // triggers call from session to controller.
+ player.notifyPlaybackState(state);
+ // triggers call from controller to session.
+ controller.play();
+
+ // Repeat above
+ player.notifyPlaybackState(state);
+ controller.pause();
+ player.notifyPlaybackState(state);
+ controller.stop();
+ player.notifyPlaybackState(state);
+ controller.skipToNext();
+ player.notifyPlaybackState(state);
+ controller.skipToPrevious();
+ }
+ // This may hang if deadlock happens.
+ latch.countDown();
+ });
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } finally {
+ if (mSession != null) {
+ sessionHandler.postAndSync(() -> {
+ // Clean up here because sessionHandler will be removed afterwards.
+ mSession.close();
+ mSession = null;
+ });
+ }
+ if (sessionThread != null) {
+ sessionThread.quitSafely();
+ }
+ if (testThread != null) {
+ testThread.quitSafely();
+ }
+ }
+ }
+
+ @Ignore
+ @Test
+ public void testGetServiceToken() {
+ SessionToken2 token = TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID);
+ assertNotNull(token);
+ assertEquals(mContext.getPackageName(), token.getPackageName());
+ assertEquals(MockMediaSessionService2.ID, token.getId());
+ assertNull(token.getSessionBinder());
+ assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+ }
+
+ private void connectToService(SessionToken2 token) throws InterruptedException {
+ mController = createController(token);
+ mSession = TestServiceRegistry.getInstance().getServiceInstance().getSession();
+ mPlayer = (MockPlayer) mSession.getPlayer();
+ }
+
+ // TODO(jaewan): Reenable when session manager detects app installs
+ @Ignore
+ @Test
+ public void testConnectToService_sessionService() throws InterruptedException {
+ connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+ testConnectToService();
+ }
+
+ // TODO(jaewan): Reenable when session manager detects app installs
+ @Ignore
+ @Test
+ public void testConnectToService_libraryService() throws InterruptedException {
+ connectToService(TestUtils.getServiceToken(mContext, MockMediaLibraryService2.ID));
+ testConnectToService();
+ }
+
+ public void testConnectToService() throws InterruptedException {
+ TestServiceRegistry serviceInfo = TestServiceRegistry.getInstance();
+ ControllerInfo info = serviceInfo.getOnConnectControllerInfo();
+ assertEquals(mContext.getPackageName(), info.getPackageName());
+ assertEquals(Process.myUid(), info.getUid());
+ assertFalse(info.isTrusted());
+
+ // Test command from controller to session service
+ mController.play();
+ assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mPlayer.mPlayCalled);
+
+ // Test command from session service to controller
+ // TODO(jaewan): Add equivalent tests again
+ /*
+ final CountDownLatch latch = new CountDownLatch(1);
+ mController.addPlaybackListener((state) -> {
+ assertNotNull(state);
+ assertEquals(PlaybackState.STATE_REWINDING, state.getState());
+ latch.countDown();
+ }, sHandler);
+ mPlayer.notifyPlaybackState(
+ TestUtils.createPlaybackState(PlaybackState.STATE_REWINDING));
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ */
+ }
+
+ @Test
+ public void testControllerAfterSessionIsGone_session() throws InterruptedException {
+ testControllerAfterSessionIsGone(mSession.getToken().getId());
+ }
+
+ @Ignore
+ @Test
+ public void testControllerAfterSessionIsGone_sessionService() throws InterruptedException {
+ connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+ testControllerAfterSessionIsGone(MockMediaSessionService2.ID);
+ }
+
+ @Test
+ public void testClose_beforeConnected() throws InterruptedException {
+ MediaController2 controller =
+ createController(mSession.getToken(), false, null);
+ controller.close();
+ }
+
+ @Test
+ public void testClose_twice() throws InterruptedException {
+ mController.close();
+ mController.close();
+ }
+
+ @Test
+ public void testClose_session() throws InterruptedException {
+ final String id = mSession.getToken().getId();
+ mController.close();
+ // close is done immediately for session.
+ testNoInteraction();
+
+ // Test whether the controller is notified about later close of the session or
+ // re-creation.
+ testControllerAfterSessionIsGone(id);
+ }
+
+ // TODO(jaewan): Reenable when session manager detects app installs
+ @Ignore
+ @Test
+ public void testClose_sessionService() throws InterruptedException {
+ connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+ testCloseFromService();
+ }
+
+ // TODO(jaewan): Reenable when session manager detects app installs
+ @Ignore
+ @Test
+ public void testClose_libraryService() throws InterruptedException {
+ connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+ testCloseFromService();
+ }
+
+ private void testCloseFromService() throws InterruptedException {
+ final String id = mController.getSessionToken().getId();
+ final CountDownLatch latch = new CountDownLatch(1);
+ TestServiceRegistry.getInstance().setServiceInstanceChangedCallback((service) -> {
+ if (service == null) {
+ // Destroying..
+ latch.countDown();
+ }
+ });
+ mController.close();
+ // Wait until close triggers onDestroy() of the session service.
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertNull(TestServiceRegistry.getInstance().getServiceInstance());
+ testNoInteraction();
+
+ // Test whether the controller is notified about later close of the session or
+ // re-creation.
+ testControllerAfterSessionIsGone(id);
+ }
+
+ private void testControllerAfterSessionIsGone(final String id) throws InterruptedException {
+ sHandler.postAndSync(() -> {
+ // TODO(jaewan): Use Session.close later when we add the API.
+ mSession.close();
+ });
+ waitForDisconnect(mController, true);
+ testNoInteraction();
+
+ // Test with the newly created session.
+ sHandler.postAndSync(() -> {
+ // Recreated session has different session stub, so previously created controller
+ // shouldn't be available.
+ mSession = new MediaSession2.Builder(mContext, mPlayer).setId(id).build();
+ });
+ testNoInteraction();
+ }
+
+ private void testNoInteraction() throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final PlaybackListener playbackListener = (state) -> {
+ fail("Controller shouldn't be notified about change in session after the close.");
+ latch.countDown();
+ };
+ // TODO(jaewan): Add equivalent tests again
+ /*
+ mController.addPlaybackListener(playbackListener, sHandler);
+ mPlayer.notifyPlaybackState(TestUtils.createPlaybackState(PlaybackState.STATE_BUFFERING));
+ assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ mController.removePlaybackListener(playbackListener);
+ */
+ }
+
+ // TODO(jaewan): Add test for service connect rejection, when we differentiate session
+ // active/inactive and connection accept/refuse
+}
diff --git a/android/media/MediaDrm.java b/android/media/MediaDrm.java
index e2f9b47..063186d 100644
--- a/android/media/MediaDrm.java
+++ b/android/media/MediaDrm.java
@@ -16,13 +16,6 @@
package android.media;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.UUID;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -33,7 +26,18 @@
import android.os.Looper;
import android.os.Message;
import android.os.Parcel;
+import android.os.PersistableBundle;
import android.util.Log;
+import dalvik.system.CloseGuard;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+
/**
* MediaDrm can be used to obtain keys for decrypting protected media streams, in
@@ -117,10 +121,13 @@
* MediaDrm objects on a thread with its own Looper running (main UI
* thread by default has a Looper running).
*/
-public final class MediaDrm {
+public final class MediaDrm implements AutoCloseable {
private static final String TAG = "MediaDrm";
+ private final AtomicBoolean mClosed = new AtomicBoolean();
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
private static final String PERMISSION = android.Manifest.permission.ACCESS_DRM_CERTIFICATES;
private EventHandler mEventHandler;
@@ -215,6 +222,8 @@
*/
native_setup(new WeakReference<MediaDrm>(this),
getByteArrayFromUUID(uuid), ActivityThread.currentOpPackageName());
+
+ mCloseGuard.open("release");
}
/**
@@ -670,12 +679,14 @@
private int mRequestType;
/**
- * Key request type is initial license request
+ * Key request type is initial license request. A license request
+ * is necessary to load keys.
*/
public static final int REQUEST_TYPE_INITIAL = 0;
/**
- * Key request type is license renewal
+ * Key request type is license renewal. A license request is
+ * necessary to prevent the keys from expiring.
*/
public static final int REQUEST_TYPE_RENEWAL = 1;
@@ -684,11 +695,25 @@
*/
public static final int REQUEST_TYPE_RELEASE = 2;
+ /**
+ * Keys are already loaded. No license request is necessary, and no
+ * key request data is returned.
+ */
+ public static final int REQUEST_TYPE_NONE = 3;
+
+ /**
+ * Keys have been loaded but an additional license request is needed
+ * to update their values.
+ */
+ public static final int REQUEST_TYPE_UPDATE = 4;
+
/** @hide */
@IntDef({
REQUEST_TYPE_INITIAL,
REQUEST_TYPE_RENEWAL,
REQUEST_TYPE_RELEASE,
+ REQUEST_TYPE_NONE,
+ REQUEST_TYPE_UPDATE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface RequestType {}
@@ -729,7 +754,8 @@
/**
* Get the type of the request
* @return one of {@link #REQUEST_TYPE_INITIAL},
- * {@link #REQUEST_TYPE_RENEWAL} or {@link #REQUEST_TYPE_RELEASE}
+ * {@link #REQUEST_TYPE_RENEWAL}, {@link #REQUEST_TYPE_RELEASE},
+ * {@link #REQUEST_TYPE_NONE} or {@link #REQUEST_TYPE_UPDATE}
*/
@RequestType
public int getRequestType() { return mRequestType; }
@@ -954,6 +980,168 @@
*/
public native void releaseAllSecureStops();
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({HDCP_LEVEL_UNKNOWN, HDCP_NONE, HDCP_V1, HDCP_V2,
+ HDCP_V2_1, HDCP_V2_2, HDCP_NO_DIGITAL_OUTPUT})
+ public @interface HdcpLevel {}
+
+
+ /**
+ * The DRM plugin did not report an HDCP level, or an error
+ * occurred accessing it
+ */
+ public static final int HDCP_LEVEL_UNKNOWN = 0;
+
+ /**
+ * HDCP is not supported on this device, content is unprotected
+ */
+ public static final int HDCP_NONE = 1;
+
+ /**
+ * HDCP version 1.0
+ */
+ public static final int HDCP_V1 = 2;
+
+ /**
+ * HDCP version 2.0 Type 1.
+ */
+ public static final int HDCP_V2 = 3;
+
+ /**
+ * HDCP version 2.1 Type 1.
+ */
+ public static final int HDCP_V2_1 = 4;
+
+ /**
+ * HDCP version 2.2 Type 1.
+ */
+ public static final int HDCP_V2_2 = 5;
+
+ /**
+ * No digital output, implicitly secure
+ */
+ public static final int HDCP_NO_DIGITAL_OUTPUT = Integer.MAX_VALUE;
+
+ /**
+ * Return the HDCP level negotiated with downstream receivers the
+ * device is connected to. If multiple HDCP-capable displays are
+ * simultaneously connected to separate interfaces, this method
+ * returns the lowest negotiated level of all interfaces.
+ * <p>
+ * This method should only be used for informational purposes, not for
+ * enforcing compliance with HDCP requirements. Trusted enforcement of
+ * HDCP policies must be handled by the DRM system.
+ * <p>
+ * @return one of {@link #HDCP_LEVEL_UNKNOWN}, {@link #HDCP_NONE},
+ * {@link #HDCP_V1}, {@link #HDCP_V2}, {@link #HDCP_V2_1}, {@link #HDCP_V2_2}
+ * or {@link #HDCP_NO_DIGITAL_OUTPUT}.
+ */
+ @HdcpLevel
+ public native int getConnectedHdcpLevel();
+
+ /**
+ * Return the maximum supported HDCP level. The maximum HDCP level is a
+ * constant for a given device, it does not depend on downstream receivers
+ * that may be connected. If multiple HDCP-capable interfaces are present,
+ * it indicates the highest of the maximum HDCP levels of all interfaces.
+ * <p>
+ * @return one of {@link #HDCP_LEVEL_UNKNOWN}, {@link #HDCP_NONE},
+ * {@link #HDCP_V1}, {@link #HDCP_V2}, {@link #HDCP_V2_1}, {@link #HDCP_V2_2}
+ * or {@link #HDCP_NO_DIGITAL_OUTPUT}.
+ */
+ @HdcpLevel
+ public native int getMaxHdcpLevel();
+
+ /**
+ * Return the number of MediaDrm sessions that are currently opened
+ * simultaneously among all MediaDrm instances for the active DRM scheme.
+ * @return the number of open sessions.
+ */
+ public native int getOpenSessionCount();
+
+ /**
+ * Return the maximum number of MediaDrm sessions that may be opened
+ * simultaneosly among all MediaDrm instances for the active DRM
+ * scheme. The maximum number of sessions is not affected by any
+ * sessions that may have already been opened.
+ * @return maximum sessions.
+ */
+ public native int getMaxSessionCount();
+
+ /**
+ * Security level indicates the robustness of the device's DRM
+ * implementation.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SECURITY_LEVEL_UNKNOWN, SW_SECURE_CRYPTO, SW_SECURE_DECODE,
+ HW_SECURE_CRYPTO, HW_SECURE_DECODE, HW_SECURE_ALL})
+ public @interface SecurityLevel {}
+
+ /**
+ * The DRM plugin did not report a security level, or an error occurred
+ * accessing it
+ */
+ public static final int SECURITY_LEVEL_UNKNOWN = 0;
+
+ /**
+ * Software-based whitebox crypto
+ */
+ public static final int SW_SECURE_CRYPTO = 1;
+
+ /**
+ * Software-based whitebox crypto and an obfuscated decoder
+ */
+ public static final int SW_SECURE_DECODE = 2;
+
+ /**
+ * DRM key management and crypto operations are performed within a
+ * hardware backed trusted execution environment
+ */
+ public static final int HW_SECURE_CRYPTO = 3;
+
+ /**
+ * DRM key management, crypto operations and decoding of content
+ * are performed within a hardware backed trusted execution environment
+ */
+ public static final int HW_SECURE_DECODE = 4;
+
+ /**
+ * DRM key management, crypto operations, decoding of content and all
+ * handling of the media (compressed and uncompressed) is handled within
+ * a hardware backed trusted execution environment.
+ */
+ public static final int HW_SECURE_ALL = 5;
+
+ /**
+ * Return the current security level of a session. A session
+ * has an initial security level determined by the robustness of
+ * the DRM system's implementation on the device. The security
+ * level may be adjusted using {@link #setSecurityLevel}.
+ * @param sessionId the session to query.
+ * <p>
+ * @return one of {@link #SECURITY_LEVEL_UNKNOWN},
+ * {@link #SW_SECURE_CRYPTO}, {@link #SW_SECURE_DECODE},
+ * {@link #HW_SECURE_CRYPTO}, {@link #HW_SECURE_DECODE} or
+ * {@link #HW_SECURE_ALL}.
+ */
+ @SecurityLevel
+ public native int getSecurityLevel(@NonNull byte[] sessionId);
+
+ /**
+ * Set the security level of a session. This can be useful if specific
+ * attributes of a lower security level are needed by an application,
+ * such as image manipulation or compositing. Reducing the security
+ * level will typically limit decryption to lower content resolutions,
+ * depending on the license policy.
+ * @param sessionId the session to set the security level on.
+ * @param level the new security level, one of
+ * {@link #SW_SECURE_CRYPTO}, {@link #SW_SECURE_DECODE},
+ * {@link #HW_SECURE_CRYPTO}, {@link #HW_SECURE_DECODE} or
+ * {@link #HW_SECURE_ALL}.
+ */
+ public native void setSecurityLevel(@NonNull byte[] sessionId,
+ @SecurityLevel int level);
+
/**
* String property name: identifies the maker of the DRM plugin
*/
@@ -1031,7 +1219,6 @@
public native void setPropertyByteArray(@NonNull @ArrayProperty
String propertyName, @NonNull byte[] value);
-
private static final native void setCipherAlgorithmNative(
@NonNull MediaDrm drm, @NonNull byte[] sessionId, @NonNull String algorithm);
@@ -1058,6 +1245,25 @@
@NonNull byte[] keyId, @NonNull byte[] message, @NonNull byte[] signature);
/**
+ * Return Metrics data about the current MediaDrm instance.
+ *
+ * @return a {@link PersistableBundle} containing the set of attributes and values
+ * available for this instance of MediaDrm.
+ * The attributes are described in {@link MetricsConstants}.
+ *
+ * Additional vendor-specific fields may also be present in
+ * the return value.
+ *
+ * @hide - not part of the public API at this time
+ */
+ public PersistableBundle getMetrics() {
+ PersistableBundle bundle = getMetricsNative();
+ return bundle;
+ }
+
+ private native PersistableBundle getMetricsNative();
+
+ /**
* In addition to supporting decryption of DASH Common Encrypted Media, the
* MediaDrm APIs provide the ability to securely deliver session keys from
* an operator's session key server to a client device, based on the factory-installed
@@ -1311,20 +1517,81 @@
}
@Override
- protected void finalize() {
- native_finalize();
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ release();
+ } finally {
+ super.finalize();
+ }
}
- public native final void release();
+ /**
+ * Releases resources associated with the current session of
+ * MediaDrm. It is considered good practice to call this method when
+ * the {@link MediaDrm} object is no longer needed in your
+ * application. After this method is called, {@link MediaDrm} is no
+ * longer usable since it has lost all of its required resource.
+ *
+ * This method was added in API 28. In API versions 18 through 27, release()
+ * should be called instead. There is no need to do anything for API
+ * versions prior to 18.
+ */
+ @Override
+ public void close() {
+ release();
+ }
+
+ /**
+ * @deprecated replaced by {@link #close()}.
+ */
+ @Deprecated
+ public void release() {
+ mCloseGuard.close();
+ if (mClosed.compareAndSet(false, true)) {
+ native_release();
+ }
+ }
+
+ /** @hide */
+ public native final void native_release();
+
private static native final void native_init();
private native final void native_setup(Object mediadrm_this, byte[] uuid,
String appPackageName);
- private native final void native_finalize();
-
static {
System.loadLibrary("media_jni");
native_init();
}
+
+ /**
+ * Definitions for the metrics that are reported via the
+ * {@link #getMetrics} call.
+ *
+ * @hide - not part of the public API at this time
+ */
+ public final static class MetricsConstants
+ {
+ private MetricsConstants() {}
+
+ /**
+ * Key to extract the number of successful {@link #openSession} calls
+ * from the {@link PersistableBundle} returned by a
+ * {@link #getMetrics} call.
+ */
+ public static final String OPEN_SESSION_OK_COUNT
+ = "/drm/mediadrm/open_session/ok/count";
+
+ /**
+ * Key to extract the number of failed {@link #openSession} calls
+ * from the {@link PersistableBundle} returned by a
+ * {@link #getMetrics} call.
+ */
+ public static final String OPEN_SESSION_ERROR_COUNT
+ = "/drm/mediadrm/open_session/error/count";
+ }
}
diff --git a/android/media/MediaFormat.java b/android/media/MediaFormat.java
index 306ed83..e9128e4 100644
--- a/android/media/MediaFormat.java
+++ b/android/media/MediaFormat.java
@@ -601,8 +601,6 @@
* codec specific, but lower values generally result in more efficient
* (smaller-sized) encoding.
*
- * @hide
- *
* @see MediaCodecInfo.EncoderCapabilities#getQualityRange()
*/
public static final String KEY_QUALITY = "quality";
@@ -680,6 +678,21 @@
public static final String KEY_LATENCY = "latency";
/**
+ * An optional key describing the maximum number of non-display-order coded frames.
+ * This is an optional parameter that applies only to video encoders. Application should
+ * check the value for this key in the output format to see if codec will produce
+ * non-display-order coded frames. If encoder supports it, the output frames' order will be
+ * different from the display order and each frame's display order could be retrived from
+ * {@link MediaCodec.BufferInfo#presentationTimeUs}. Before API level 27, application may
+ * receive non-display-order coded frames even though the application did not request it.
+ * Note: Application should not rearrange the frames to display order before feeding them
+ * to {@link MediaMuxer#writeSampleData}.
+ * <p>
+ * The default value is 0.
+ */
+ public static final String KEY_OUTPUT_REORDER_DEPTH = "output-reorder-depth";
+
+ /**
* A key describing the desired clockwise rotation on an output surface.
* This key is only used when the codec is configured using an output surface.
* The associated value is an integer, representing degrees. Supported values
diff --git a/android/media/MediaItem2.java b/android/media/MediaItem2.java
new file mode 100644
index 0000000..96a87d5
--- /dev/null
+++ b/android/media/MediaItem2.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A class with information on a single media item with the metadata information.
+ * Media item are application dependent so we cannot guarantee that they contain the right values.
+ * <p>
+ * When it's sent to a controller or browser, it's anonymized and data descriptor wouldn't be sent.
+ * <p>
+ * This object isn't a thread safe.
+ *
+ * @hide
+ */
+// TODO(jaewan): Unhide and extends from DataSourceDesc.
+// Note) Feels like an anti-pattern. We should anonymize MediaItem2 to remove *all*
+// information in the DataSourceDesc. Why it should extends from this?
+// TODO(jaewan): Move this to updatable
+// Previously MediaBrowser.MediaItem
+public class MediaItem2 {
+ private final int mFlags;
+ private MediaMetadata2 mMetadata;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
+ public @interface Flags { }
+
+ /**
+ * Flag: Indicates that the item has children of its own.
+ */
+ public static final int FLAG_BROWSABLE = 1 << 0;
+
+ /**
+ * Flag: Indicates that the item is playable.
+ * <p>
+ * The id of this item may be passed to
+ * {@link MediaController2#playFromMediaId(String, Bundle)}
+ */
+ public static final int FLAG_PLAYABLE = 1 << 1;
+
+ /**
+ * Create a new media item.
+ *
+ * @param metadata metadata with the media id.
+ * @param flags The flags for this item.
+ */
+ public MediaItem2(@NonNull MediaMetadata2 metadata, @Flags int flags) {
+ mFlags = flags;
+ setMetadata(metadata);
+ }
+
+ /**
+ * Return this object as a bundle to share between processes.
+ *
+ * @return a new bundle instance
+ */
+ public Bundle toBundle() {
+ // TODO(jaewan): Fill here when we rebase.
+ return new Bundle();
+ }
+
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("MediaItem2{");
+ sb.append("mFlags=").append(mFlags);
+ sb.append(", mMetadata=").append(mMetadata);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ /**
+ * Gets the flags of the item.
+ */
+ public @Flags int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Returns whether this item is browsable.
+ * @see #FLAG_BROWSABLE
+ */
+ public boolean isBrowsable() {
+ return (mFlags & FLAG_BROWSABLE) != 0;
+ }
+
+ /**
+ * Returns whether this item is playable.
+ * @see #FLAG_PLAYABLE
+ */
+ public boolean isPlayable() {
+ return (mFlags & FLAG_PLAYABLE) != 0;
+ }
+
+ /**
+ * Set a metadata. Metadata shouldn't be null and should have non-empty media id.
+ *
+ * @param metadata
+ */
+ public void setMetadata(@NonNull MediaMetadata2 metadata) {
+ if (metadata == null) {
+ throw new IllegalArgumentException("metadata cannot be null");
+ }
+ if (TextUtils.isEmpty(metadata.getMediaId())) {
+ throw new IllegalArgumentException("metadata must have a non-empty media id");
+ }
+ mMetadata = metadata;
+ }
+
+ /**
+ * Returns the metadata of the media.
+ */
+ public @NonNull MediaMetadata2 getMetadata() {
+ return mMetadata;
+ }
+
+ /**
+ * Returns the media id in the {@link MediaMetadata2} for this item.
+ * @see MediaMetadata2#METADATA_KEY_MEDIA_ID
+ */
+ public @Nullable String getMediaId() {
+ return mMetadata.getMediaId();
+ }
+}
diff --git a/android/media/MediaLibraryService2.java b/android/media/MediaLibraryService2.java
new file mode 100644
index 0000000..d7e43ec
--- /dev/null
+++ b/android/media/MediaLibraryService2.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.media.MediaSession2.BuilderBase;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.update.ApiLoader;
+import android.media.update.MediaLibraryService2Provider.MediaLibrarySessionProvider;
+import android.media.update.MediaSession2Provider;
+import android.media.update.MediaSessionService2Provider;
+import android.os.Bundle;
+import android.service.media.MediaBrowserService.BrowserRoot;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Base class for media library services.
+ * <p>
+ * Media library services enable applications to browse media content provided by an application
+ * and ask the application to start playing it. They may also be used to control content that
+ * is already playing by way of a {@link MediaSession2}.
+ * <p>
+ * To extend this class, adding followings directly to your {@code AndroidManifest.xml}.
+ * <pre>
+ * <service android:name="component_name_of_your_implementation" >
+ * <intent-filter>
+ * <action android:name="android.media.MediaLibraryService2" />
+ * </intent-filter>
+ * </service></pre>
+ * <p>
+ * A {@link MediaLibraryService2} is extension of {@link MediaSessionService2}. IDs shouldn't
+ * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By
+ * default, an empty string will be used for ID of the service. If you want to specify an ID,
+ * declare metadata in the manifest as follows.
+ * @hide
+ */
+// TODO(jaewan): Unhide
+public abstract class MediaLibraryService2 extends MediaSessionService2 {
+ /**
+ * This is the interface name that a service implementing a session service should say that it
+ * support -- that is, this is the action it uses for its intent filter.
+ */
+ public static final String SERVICE_INTERFACE = "android.media.MediaLibraryService2";
+
+ /**
+ * Session for the media library service.
+ */
+ public class MediaLibrarySession extends MediaSession2 {
+ private final MediaLibrarySessionProvider mProvider;
+
+ MediaLibrarySession(Context context, MediaPlayerBase player, String id,
+ Executor callbackExecutor, SessionCallback callback, VolumeProvider volumeProvider,
+ int ratingType, PendingIntent sessionActivity) {
+ super(context, player, id, callbackExecutor, callback, volumeProvider, ratingType,
+ sessionActivity);
+ mProvider = (MediaLibrarySessionProvider) getProvider();
+ }
+
+ @Override
+ MediaSession2Provider createProvider(Context context, MediaPlayerBase player, String id,
+ Executor callbackExecutor, SessionCallback callback, VolumeProvider volumeProvider,
+ int ratingType, PendingIntent sessionActivity) {
+ return ApiLoader.getProvider(context)
+ .createMediaLibraryService2MediaLibrarySession(this, context, player, id,
+ callbackExecutor, (MediaLibrarySessionCallback) callback,
+ volumeProvider, ratingType, sessionActivity);
+ }
+
+ /**
+ * Notify subscribed controller about change in a parent's children.
+ *
+ * @param controller controller to notify
+ * @param parentId
+ * @param options
+ */
+ public void notifyChildrenChanged(@NonNull ControllerInfo controller,
+ @NonNull String parentId, @NonNull Bundle options) {
+ mProvider.notifyChildrenChanged_impl(controller, parentId, options);
+ }
+
+ /**
+ * Notify subscribed controller about change in a parent's children.
+ *
+ * @param parentId parent id
+ * @param options optional bundle
+ */
+ // This is for the backward compatibility.
+ public void notifyChildrenChanged(@NonNull String parentId, @Nullable Bundle options) {
+ mProvider.notifyChildrenChanged_impl(parentId, options);
+ }
+ }
+
+ public static class MediaLibrarySessionCallback extends MediaSession2.SessionCallback {
+ /**
+ * Called to get the root information for browsing by a particular client.
+ * <p>
+ * The implementation should verify that the client package has permission
+ * to access browse media information before returning the root id; it
+ * should return null if the client is not allowed to access this
+ * information.
+ *
+ * @param controllerInfo information of the controller requesting access to browse media.
+ * @param rootHints An optional bundle of service-specific arguments to send
+ * to the media browser service when connecting and retrieving the
+ * root id for browsing, or null if none. The contents of this
+ * bundle may affect the information returned when browsing.
+ * @return The {@link BrowserRoot} for accessing this app's content or null.
+ * @see BrowserRoot#EXTRA_RECENT
+ * @see BrowserRoot#EXTRA_OFFLINE
+ * @see BrowserRoot#EXTRA_SUGGESTED
+ */
+ public @Nullable BrowserRoot onGetRoot(@NonNull ControllerInfo controllerInfo,
+ @Nullable Bundle rootHints) {
+ return null;
+ }
+
+ /**
+ * Called to get the search result. Return search result here for the browser.
+ * <p>
+ * Return an empty list for no search result, and return {@code null} for the error.
+ *
+ * @param query The search query sent from the media browser. It contains keywords separated
+ * by space.
+ * @param extras The bundle of service-specific arguments sent from the media browser.
+ * @return search result. {@code null} for error.
+ */
+ public @Nullable List<MediaItem2> onSearch(@NonNull ControllerInfo controllerInfo,
+ @NonNull String query, @Nullable Bundle extras) {
+ return null;
+ }
+
+ /**
+ * Called to get the search result . Return result here for the browser.
+ * <p>
+ * Return an empty list for no search result, and return {@code null} for the error.
+ *
+ * @param itemId item id to get media item.
+ * @return media item2. {@code null} for error.
+ */
+ public @Nullable MediaItem2 onLoadItem(@NonNull ControllerInfo controllerInfo,
+ @NonNull String itemId) {
+ return null;
+ }
+
+ /**
+ * Called to get the search result. Return search result here for the browser.
+ * <p>
+ * Return an empty list for no search result, and return {@code null} for the error.
+ *
+ * @param parentId parent id to get children
+ * @param page number of page
+ * @param pageSize size of the page
+ * @param options
+ * @return list of children. Can be {@code null}.
+ */
+ public @Nullable List<MediaItem2> onLoadChildren(@NonNull ControllerInfo controller,
+ @NonNull String parentId, int page, int pageSize, @Nullable Bundle options) {
+ return null;
+ }
+
+ /**
+ * Called when a controller subscribes to the parent.
+ *
+ * @param controller controller
+ * @param parentId parent id
+ * @param options optional bundle
+ */
+ public void onSubscribed(@NonNull ControllerInfo controller,
+ String parentId, @Nullable Bundle options) {
+ }
+
+ /**
+ * Called when a controller unsubscribes to the parent.
+ *
+ * @param controller controller
+ * @param parentId parent id
+ * @param options optional bundle
+ */
+ public void onUnsubscribed(@NonNull ControllerInfo controller,
+ String parentId, @Nullable Bundle options) {
+ }
+ }
+
+ /**
+ * Builder for {@link MediaLibrarySession}.
+ */
+ // TODO(jaewan): Move this to updatable.
+ public class MediaLibrarySessionBuilder
+ extends BuilderBase<MediaLibrarySessionBuilder, MediaLibrarySessionCallback> {
+ public MediaLibrarySessionBuilder(
+ @NonNull Context context, @NonNull MediaPlayerBase player,
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull MediaLibrarySessionCallback callback) {
+ super(context, player);
+ setSessionCallback(callbackExecutor, callback);
+ }
+
+ @Override
+ public MediaLibrarySessionBuilder setSessionCallback(
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull MediaLibrarySessionCallback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("MediaLibrarySessionCallback cannot be null");
+ }
+ return super.setSessionCallback(callbackExecutor, callback);
+ }
+
+ @Override
+ public MediaLibrarySession build() throws IllegalStateException {
+ return new MediaLibrarySession(mContext, mPlayer, mId, mCallbackExecutor, mCallback,
+ mVolumeProvider, mRatingType, mSessionActivity);
+ }
+ }
+
+ @Override
+ MediaSessionService2Provider createProvider() {
+ return ApiLoader.getProvider(this).createMediaLibraryService2(this);
+ }
+
+ /**
+ * Called when another app requested to start this service.
+ * <p>
+ * Library service will accept or reject the connection with the
+ * {@link MediaLibrarySessionCallback} in the created session.
+ * <p>
+ * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the
+ * expected ID that you've specified through the AndroidManifest.xml.
+ * <p>
+ * This method will be called on the main thread.
+ *
+ * @param sessionId session id written in the AndroidManifest.xml.
+ * @return a new browser session
+ * @see MediaLibrarySessionBuilder
+ * @see #getSession()
+ * @throws RuntimeException if returned session is invalid
+ */
+ @Override
+ public @NonNull abstract MediaLibrarySession onCreateSession(String sessionId);
+
+ /**
+ * Contains information that the browser service needs to send to the client
+ * when first connected.
+ */
+ public static final class BrowserRoot {
+ /**
+ * The lookup key for a boolean that indicates whether the browser service should return a
+ * browser root for recently played media items.
+ *
+ * <p>When creating a media browser for a given media browser service, this key can be
+ * supplied as a root hint for retrieving media items that are recently played.
+ * If the media browser service can provide such media items, the implementation must return
+ * the key in the root hint when
+ * {@link MediaLibrarySessionCallback#onGetRoot(ControllerInfo, Bundle)} is called back.
+ *
+ * <p>The root hint may contain multiple keys.
+ *
+ * @see #EXTRA_OFFLINE
+ * @see #EXTRA_SUGGESTED
+ */
+ public static final String EXTRA_RECENT = "android.service.media.extra.RECENT";
+
+ /**
+ * The lookup key for a boolean that indicates whether the browser service should return a
+ * browser root for offline media items.
+ *
+ * <p>When creating a media browser for a given media browser service, this key can be
+ * supplied as a root hint for retrieving media items that are can be played without an
+ * internet connection.
+ * If the media browser service can provide such media items, the implementation must return
+ * the key in the root hint when
+ * {@link MediaLibrarySessionCallback#onGetRoot(ControllerInfo, Bundle)} is called back.
+ *
+ * <p>The root hint may contain multiple keys.
+ *
+ * @see #EXTRA_RECENT
+ * @see #EXTRA_SUGGESTED
+ */
+ public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
+
+ /**
+ * The lookup key for a boolean that indicates whether the browser service should return a
+ * browser root for suggested media items.
+ *
+ * <p>When creating a media browser for a given media browser service, this key can be
+ * supplied as a root hint for retrieving the media items suggested by the media browser
+ * service. The list of media items passed in {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)}
+ * is considered ordered by relevance, first being the top suggestion.
+ * If the media browser service can provide such media items, the implementation must return
+ * the key in the root hint when
+ * {@link MediaLibrarySessionCallback#onGetRoot(ControllerInfo, Bundle)} is called back.
+ *
+ * <p>The root hint may contain multiple keys.
+ *
+ * @see #EXTRA_RECENT
+ * @see #EXTRA_OFFLINE
+ */
+ public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
+
+ final private String mRootId;
+ final private Bundle mExtras;
+
+ /**
+ * Constructs a browser root.
+ * @param rootId The root id for browsing.
+ * @param extras Any extras about the browser service.
+ */
+ public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
+ if (rootId == null) {
+ throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " +
+ "Use null for BrowserRoot instead.");
+ }
+ mRootId = rootId;
+ mExtras = extras;
+ }
+
+ /**
+ * Gets the root id for browsing.
+ */
+ public String getRootId() {
+ return mRootId;
+ }
+
+ /**
+ * Gets any extras about the browser service.
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+ }
+}
diff --git a/android/media/MediaMetadata2.java b/android/media/MediaMetadata2.java
new file mode 100644
index 0000000..0e24db6
--- /dev/null
+++ b/android/media/MediaMetadata2.java
@@ -0,0 +1,815 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.ArrayMap;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Set;
+
+/**
+ * Contains metadata about an item, such as the title, artist, etc.
+ * @hide
+ */
+// TODO(jaewan): Move this to updatable
+public final class MediaMetadata2 {
+ private static final String TAG = "MediaMetadata2";
+
+ /**
+ * The title of the media.
+ */
+ public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE";
+
+ /**
+ * The artist of the media.
+ */
+ public static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST";
+
+ /**
+ * The duration of the media in ms. A negative duration indicates that the
+ * duration is unknown (or infinite).
+ */
+ public static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION";
+
+ /**
+ * The album title for the media.
+ */
+ public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM";
+
+ /**
+ * The author of the media.
+ */
+ public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR";
+
+ /**
+ * The writer of the media.
+ */
+ public static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER";
+
+ /**
+ * The composer of the media.
+ */
+ public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER";
+
+ /**
+ * The compilation status of the media.
+ */
+ public static final String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION";
+
+ /**
+ * The date the media was created or published. The format is unspecified
+ * but RFC 3339 is recommended.
+ */
+ public static final String METADATA_KEY_DATE = "android.media.metadata.DATE";
+
+ /**
+ * The year the media was created or published as a long.
+ */
+ public static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR";
+
+ /**
+ * The genre of the media.
+ */
+ public static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE";
+
+ /**
+ * The track number for the media.
+ */
+ public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
+
+ /**
+ * The number of tracks in the media's original source.
+ */
+ public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS";
+
+ /**
+ * The disc number for the media's original source.
+ */
+ public static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
+
+ /**
+ * The artist for the album of the media's original source.
+ */
+ public static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
+
+ /**
+ * The artwork for the media as a {@link Bitmap}.
+ *
+ * The artwork should be relatively small and may be scaled down
+ * if it is too large. For higher resolution artwork
+ * {@link #METADATA_KEY_ART_URI} should be used instead.
+ */
+ public static final String METADATA_KEY_ART = "android.media.metadata.ART";
+
+ /**
+ * The artwork for the media as a Uri style String.
+ */
+ public static final String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI";
+
+ /**
+ * The artwork for the album of the media's original source as a
+ * {@link Bitmap}.
+ * The artwork should be relatively small and may be scaled down
+ * if it is too large. For higher resolution artwork
+ * {@link #METADATA_KEY_ALBUM_ART_URI} should be used instead.
+ */
+ public static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART";
+
+ /**
+ * The artwork for the album of the media's original source as a Uri style
+ * String.
+ */
+ public static final String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI";
+
+ /**
+ * The user's rating for the media.
+ *
+ * @see Rating
+ */
+ public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING";
+
+ /**
+ * The overall rating for the media.
+ *
+ * @see Rating
+ */
+ public static final String METADATA_KEY_RATING = "android.media.metadata.RATING";
+
+ /**
+ * A title that is suitable for display to the user. This will generally be
+ * the same as {@link #METADATA_KEY_TITLE} but may differ for some formats.
+ * When displaying media described by this metadata this should be preferred
+ * if present.
+ */
+ public static final String METADATA_KEY_DISPLAY_TITLE = "android.media.metadata.DISPLAY_TITLE";
+
+ /**
+ * A subtitle that is suitable for display to the user. When displaying a
+ * second line for media described by this metadata this should be preferred
+ * to other fields if present.
+ */
+ public static final String METADATA_KEY_DISPLAY_SUBTITLE
+ = "android.media.metadata.DISPLAY_SUBTITLE";
+
+ /**
+ * A description that is suitable for display to the user. When displaying
+ * more information for media described by this metadata this should be
+ * preferred to other fields if present.
+ */
+ public static final String METADATA_KEY_DISPLAY_DESCRIPTION
+ = "android.media.metadata.DISPLAY_DESCRIPTION";
+
+ /**
+ * An icon or thumbnail that is suitable for display to the user. When
+ * displaying an icon for media described by this metadata this should be
+ * preferred to other fields if present. This must be a {@link Bitmap}.
+ *
+ * The icon should be relatively small and may be scaled down
+ * if it is too large. For higher resolution artwork
+ * {@link #METADATA_KEY_DISPLAY_ICON_URI} should be used instead.
+ */
+ public static final String METADATA_KEY_DISPLAY_ICON
+ = "android.media.metadata.DISPLAY_ICON";
+
+ /**
+ * An icon or thumbnail that is suitable for display to the user. When
+ * displaying more information for media described by this metadata the
+ * display description should be preferred to other fields when present.
+ * This must be a Uri style String.
+ */
+ public static final String METADATA_KEY_DISPLAY_ICON_URI
+ = "android.media.metadata.DISPLAY_ICON_URI";
+
+ /**
+ * A String key for identifying the content. This value is specific to the
+ * service providing the content. If used, this should be a persistent
+ * unique key for the underlying content. It may be used with
+ * {@link MediaController2#playFromMediaId(String, Bundle)}
+ * to initiate playback when provided by a {@link MediaBrowser2} connected to
+ * the same app.
+ */
+ public static final String METADATA_KEY_MEDIA_ID = "android.media.metadata.MEDIA_ID";
+
+ /**
+ * A Uri formatted String representing the content. This value is specific to the
+ * service providing the content. It may be used with
+ * {@link MediaController2#playFromUri(Uri, Bundle)}
+ * to initiate playback when provided by a {@link MediaBrowser2} connected to
+ * the same app.
+ */
+ public static final String METADATA_KEY_MEDIA_URI = "android.media.metadata.MEDIA_URI";
+
+ /**
+ * The bluetooth folder type of the media specified in the section 6.10.2.2 of the Bluetooth
+ * AVRCP 1.5. It should be one of the following:
+ * <ul>
+ * <li>{@link #BT_FOLDER_TYPE_MIXED}</li>
+ * <li>{@link #BT_FOLDER_TYPE_TITLES}</li>
+ * <li>{@link #BT_FOLDER_TYPE_ALBUMS}</li>
+ * <li>{@link #BT_FOLDER_TYPE_ARTISTS}</li>
+ * <li>{@link #BT_FOLDER_TYPE_GENRES}</li>
+ * <li>{@link #BT_FOLDER_TYPE_PLAYLISTS}</li>
+ * <li>{@link #BT_FOLDER_TYPE_YEARS}</li>
+ * </ul>
+ */
+ public static final String METADATA_KEY_BT_FOLDER_TYPE
+ = "android.media.metadata.BT_FOLDER_TYPE";
+
+ /**
+ * The type of folder that is unknown or contains media elements of mixed types as specified in
+ * the section 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_MIXED = 0;
+
+ /**
+ * The type of folder that contains media elements only as specified in the section 6.10.2.2 of
+ * the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_TITLES = 1;
+
+ /**
+ * The type of folder that contains folders categorized by album as specified in the section
+ * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_ALBUMS = 2;
+
+ /**
+ * The type of folder that contains folders categorized by artist as specified in the section
+ * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_ARTISTS = 3;
+
+ /**
+ * The type of folder that contains folders categorized by genre as specified in the section
+ * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_GENRES = 4;
+
+ /**
+ * The type of folder that contains folders categorized by playlist as specified in the section
+ * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_PLAYLISTS = 5;
+
+ /**
+ * The type of folder that contains folders categorized by year as specified in the section
+ * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_YEARS = 6;
+
+ /**
+ * Whether the media is an advertisement. A value of 0 indicates it is not an advertisement. A
+ * value of 1 or non-zero indicates it is an advertisement. If not specified, this value is set
+ * to 0 by default.
+ */
+ public static final String METADATA_KEY_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT";
+
+ /**
+ * The download status of the media which will be used for later offline playback. It should be
+ * one of the following:
+ *
+ * <ul>
+ * <li>{@link #STATUS_NOT_DOWNLOADED}</li>
+ * <li>{@link #STATUS_DOWNLOADING}</li>
+ * <li>{@link #STATUS_DOWNLOADED}</li>
+ * </ul>
+ */
+ public static final String METADATA_KEY_DOWNLOAD_STATUS =
+ "android.media.metadata.DOWNLOAD_STATUS";
+
+ /**
+ * The status value to indicate the media item is not downloaded.
+ *
+ * @see #METADATA_KEY_DOWNLOAD_STATUS
+ */
+ public static final long STATUS_NOT_DOWNLOADED = 0;
+
+ /**
+ * The status value to indicate the media item is being downloaded.
+ *
+ * @see #METADATA_KEY_DOWNLOAD_STATUS
+ */
+ public static final long STATUS_DOWNLOADING = 1;
+
+ /**
+ * The status value to indicate the media item is downloaded for later offline playback.
+ *
+ * @see #METADATA_KEY_DOWNLOAD_STATUS
+ */
+ public static final long STATUS_DOWNLOADED = 2;
+
+ /**
+ * A {@link Bundle} extra.
+ * @hide
+ */
+ public static final String METADATA_KEY_EXTRA = "android.media.metadata.EXTRA";
+
+ /**
+ * @hide
+ */
+ @StringDef({METADATA_KEY_TITLE, METADATA_KEY_ARTIST, METADATA_KEY_ALBUM, METADATA_KEY_AUTHOR,
+ METADATA_KEY_WRITER, METADATA_KEY_COMPOSER, METADATA_KEY_COMPILATION,
+ METADATA_KEY_DATE, METADATA_KEY_GENRE, METADATA_KEY_ALBUM_ARTIST, METADATA_KEY_ART_URI,
+ METADATA_KEY_ALBUM_ART_URI, METADATA_KEY_DISPLAY_TITLE, METADATA_KEY_DISPLAY_SUBTITLE,
+ METADATA_KEY_DISPLAY_DESCRIPTION, METADATA_KEY_DISPLAY_ICON_URI,
+ METADATA_KEY_MEDIA_ID, METADATA_KEY_MEDIA_URI})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TextKey {}
+
+ /**
+ * @hide
+ */
+ @StringDef({METADATA_KEY_DURATION, METADATA_KEY_YEAR, METADATA_KEY_TRACK_NUMBER,
+ METADATA_KEY_NUM_TRACKS, METADATA_KEY_DISC_NUMBER, METADATA_KEY_BT_FOLDER_TYPE,
+ METADATA_KEY_ADVERTISEMENT, METADATA_KEY_DOWNLOAD_STATUS})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LongKey {}
+
+ /**
+ * @hide
+ */
+ @StringDef({METADATA_KEY_ART, METADATA_KEY_ALBUM_ART, METADATA_KEY_DISPLAY_ICON})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BitmapKey {}
+
+ /**
+ * @hide
+ */
+ @StringDef({METADATA_KEY_USER_RATING, METADATA_KEY_RATING})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RatingKey {}
+
+ static final int METADATA_TYPE_LONG = 0;
+ static final int METADATA_TYPE_TEXT = 1;
+ static final int METADATA_TYPE_BITMAP = 2;
+ static final int METADATA_TYPE_RATING = 3;
+ static final ArrayMap<String, Integer> METADATA_KEYS_TYPE;
+
+ static {
+ METADATA_KEYS_TYPE = new ArrayMap<String, Integer>();
+ METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DURATION, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_AUTHOR, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_WRITER, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_COMPOSER, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_COMPILATION, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DATE, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_YEAR, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_TRACK_NUMBER, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_NUM_TRACKS, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DISC_NUMBER, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ARTIST, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ART_URI, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART, METADATA_TYPE_BITMAP);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART_URI, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_USER_RATING, METADATA_TYPE_RATING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_RATING, METADATA_TYPE_RATING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_TITLE, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_SUBTITLE, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_DESCRIPTION, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON, METADATA_TYPE_BITMAP);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON_URI, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_ID, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_BT_FOLDER_TYPE, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_URI, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ADVERTISEMENT, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DOWNLOAD_STATUS, METADATA_TYPE_LONG);
+ }
+
+ private static final @TextKey String[] PREFERRED_DESCRIPTION_ORDER = {
+ METADATA_KEY_TITLE,
+ METADATA_KEY_ARTIST,
+ METADATA_KEY_ALBUM,
+ METADATA_KEY_ALBUM_ARTIST,
+ METADATA_KEY_WRITER,
+ METADATA_KEY_AUTHOR,
+ METADATA_KEY_COMPOSER
+ };
+
+ private static final @BitmapKey String[] PREFERRED_BITMAP_ORDER = {
+ METADATA_KEY_DISPLAY_ICON,
+ METADATA_KEY_ART,
+ METADATA_KEY_ALBUM_ART
+ };
+
+ private static final @TextKey String[] PREFERRED_URI_ORDER = {
+ METADATA_KEY_DISPLAY_ICON_URI,
+ METADATA_KEY_ART_URI,
+ METADATA_KEY_ALBUM_ART_URI
+ };
+
+ final Bundle mBundle;
+
+ /**
+ * @hide
+ */
+ public MediaMetadata2(Bundle bundle) {
+ mBundle = new Bundle(bundle);
+ }
+
+ /**
+ * Returns true if the given key is contained in the metadata
+ *
+ * @param key a String key
+ * @return true if the key exists in this metadata, false otherwise
+ */
+ public boolean containsKey(String key) {
+ return mBundle.containsKey(key);
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if no mapping of
+ * the desired type exists for the given key or a null value is explicitly
+ * associated with the key.
+ *
+ * @param key The key the value is stored under
+ * @return a CharSequence value, or null
+ */
+ public CharSequence getText(@TextKey String key) {
+ return mBundle.getCharSequence(key);
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if no mapping of
+ * the desired type exists for the given key or a null value is explicitly
+ * associated with the key.
+ *
+ * @
+ * @return media id. Can be {@code null}
+ */
+ public @Nullable String getMediaId() {
+ return getString(METADATA_KEY_MEDIA_ID);
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if no mapping of
+ * the desired type exists for the given key or a null value is explicitly
+ * associated with the key.
+ *
+ * @param key The key the value is stored under
+ * @return a String value, or null
+ */
+ public String getString(@TextKey String key) {
+ CharSequence text = mBundle.getCharSequence(key);
+ if (text != null) {
+ return text.toString();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the value associated with the given key, or 0L if no long exists
+ * for the given key.
+ *
+ * @param key The key the value is stored under
+ * @return a long value
+ */
+ public long getLong(@LongKey String key) {
+ return mBundle.getLong(key, 0);
+ }
+
+ /**
+ * Return a {@link Rating2} for the given key or null if no rating exists for
+ * the given key.
+ *
+ * @param key The key the value is stored under
+ * @return A {@link Rating2} or null
+ */
+ public Rating2 getRating(@RatingKey String key) {
+ // TODO(jaewan): Add backward compatibility
+ Rating2 rating = null;
+ try {
+ rating = Rating2.fromBundle(mBundle.getBundle(key));
+ } catch (Exception e) {
+ // ignore, value was not a rating
+ Log.w(TAG, "Failed to retrieve a key as Rating.", e);
+ }
+ return rating;
+ }
+
+ /**
+ * Return a {@link Bitmap} for the given key or null if no bitmap exists for
+ * the given key.
+ *
+ * @param key The key the value is stored under
+ * @return A {@link Bitmap} or null
+ */
+ public Bitmap getBitmap(@BitmapKey String key) {
+ Bitmap bmp = null;
+ try {
+ bmp = mBundle.getParcelable(key);
+ } catch (Exception e) {
+ // ignore, value was not a bitmap
+ Log.w(TAG, "Failed to retrieve a key as Bitmap.", e);
+ }
+ return bmp;
+ }
+
+ /**
+ * Get the extra {@link Bundle} from the metadata object.
+ *
+ * @return A {@link Bundle} or {@code null}
+ */
+ public Bundle getExtra() {
+ try {
+ return mBundle.getBundle(METADATA_KEY_EXTRA);
+ } catch (Exception e) {
+ // ignore, value was not an bundle
+ Log.w(TAG, "Failed to retrieve an extra");
+ }
+ return null;
+ }
+
+ /**
+ * Get the number of fields in this metadata.
+ *
+ * @return The number of fields in the metadata.
+ */
+ public int size() {
+ return mBundle.size();
+ }
+
+ /**
+ * Returns a Set containing the Strings used as keys in this metadata.
+ *
+ * @return a Set of String keys
+ */
+ public Set<String> keySet() {
+ return mBundle.keySet();
+ }
+
+ /**
+ * Gets the bundle backing the metadata object. This is available to support
+ * backwards compatibility. Apps should not modify the bundle directly.
+ *
+ * @return The Bundle backing this metadata.
+ */
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Use to build MediaMetadata2 objects. The system defined metadata keys must
+ * use the appropriate data type.
+ */
+ public static final class Builder {
+ private final Bundle mBundle;
+
+ /**
+ * Create an empty Builder. Any field that should be included in the
+ * {@link MediaMetadata2} must be added.
+ */
+ public Builder() {
+ mBundle = new Bundle();
+ }
+
+ /**
+ * Create a Builder using a {@link MediaMetadata2} instance to set the
+ * initial values. All fields in the source metadata will be included in
+ * the new metadata. Fields can be overwritten by adding the same key.
+ *
+ * @param source
+ */
+ public Builder(MediaMetadata2 source) {
+ mBundle = new Bundle(source.mBundle);
+ }
+
+ /**
+ * Create a Builder using a {@link MediaMetadata2} instance to set
+ * initial values, but replace bitmaps with a scaled down copy if they
+ * are larger than maxBitmapSize.
+ *
+ * @param source The original metadata to copy.
+ * @param maxBitmapSize The maximum height/width for bitmaps contained
+ * in the metadata.
+ * @hide
+ */
+ public Builder(MediaMetadata2 source, int maxBitmapSize) {
+ this(source);
+ for (String key : mBundle.keySet()) {
+ Object value = mBundle.get(key);
+ if (value instanceof Bitmap) {
+ Bitmap bmp = (Bitmap) value;
+ if (bmp.getHeight() > maxBitmapSize || bmp.getWidth() > maxBitmapSize) {
+ putBitmap(key, scaleBitmap(bmp, maxBitmapSize));
+ }
+ }
+ }
+ }
+
+ /**
+ * Put a CharSequence value into the metadata. Custom keys may be used,
+ * but if the METADATA_KEYs defined in this class are used they may only
+ * be one of the following:
+ * <ul>
+ * <li>{@link #METADATA_KEY_TITLE}</li>
+ * <li>{@link #METADATA_KEY_ARTIST}</li>
+ * <li>{@link #METADATA_KEY_ALBUM}</li>
+ * <li>{@link #METADATA_KEY_AUTHOR}</li>
+ * <li>{@link #METADATA_KEY_WRITER}</li>
+ * <li>{@link #METADATA_KEY_COMPOSER}</li>
+ * <li>{@link #METADATA_KEY_DATE}</li>
+ * <li>{@link #METADATA_KEY_GENRE}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ARTIST}</li>
+ * <li>{@link #METADATA_KEY_ART_URI}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ART_URI}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_TITLE}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_SUBTITLE}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_DESCRIPTION}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_ICON_URI}</li>
+ * </ul>
+ *
+ * @param key The key for referencing this value
+ * @param value The CharSequence value to store
+ * @return The Builder to allow chaining
+ */
+ public Builder putText(@TextKey String key, CharSequence value) {
+ if (METADATA_KEYS_TYPE.containsKey(key)) {
+ if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) {
+ throw new IllegalArgumentException("The " + key
+ + " key cannot be used to put a CharSequence");
+ }
+ }
+ mBundle.putCharSequence(key, value);
+ return this;
+ }
+
+ /**
+ * Put a String value into the metadata. Custom keys may be used, but if
+ * the METADATA_KEYs defined in this class are used they may only be one
+ * of the following:
+ * <ul>
+ * <li>{@link #METADATA_KEY_TITLE}</li>
+ * <li>{@link #METADATA_KEY_ARTIST}</li>
+ * <li>{@link #METADATA_KEY_ALBUM}</li>
+ * <li>{@link #METADATA_KEY_AUTHOR}</li>
+ * <li>{@link #METADATA_KEY_WRITER}</li>
+ * <li>{@link #METADATA_KEY_COMPOSER}</li>
+ * <li>{@link #METADATA_KEY_DATE}</li>
+ * <li>{@link #METADATA_KEY_GENRE}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ARTIST}</li>
+ * <li>{@link #METADATA_KEY_ART_URI}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ART_URI}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_TITLE}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_SUBTITLE}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_DESCRIPTION}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_ICON_URI}</li>
+ * </ul>
+ *
+ * @param key The key for referencing this value
+ * @param value The String value to store
+ * @return The Builder to allow chaining
+ */
+ public Builder putString(@TextKey String key, String value) {
+ if (METADATA_KEYS_TYPE.containsKey(key)) {
+ if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) {
+ throw new IllegalArgumentException("The " + key
+ + " key cannot be used to put a String");
+ }
+ }
+ mBundle.putCharSequence(key, value);
+ return this;
+ }
+
+ /**
+ * Put a long value into the metadata. Custom keys may be used, but if
+ * the METADATA_KEYs defined in this class are used they may only be one
+ * of the following:
+ * <ul>
+ * <li>{@link #METADATA_KEY_DURATION}</li>
+ * <li>{@link #METADATA_KEY_TRACK_NUMBER}</li>
+ * <li>{@link #METADATA_KEY_NUM_TRACKS}</li>
+ * <li>{@link #METADATA_KEY_DISC_NUMBER}</li>
+ * <li>{@link #METADATA_KEY_YEAR}</li>
+ * <li>{@link #METADATA_KEY_BT_FOLDER_TYPE}</li>
+ * <li>{@link #METADATA_KEY_ADVERTISEMENT}</li>
+ * <li>{@link #METADATA_KEY_DOWNLOAD_STATUS}</li>
+ * </ul>
+ *
+ * @param key The key for referencing this value
+ * @param value The String value to store
+ * @return The Builder to allow chaining
+ */
+ public Builder putLong(@LongKey String key, long value) {
+ if (METADATA_KEYS_TYPE.containsKey(key)) {
+ if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_LONG) {
+ throw new IllegalArgumentException("The " + key
+ + " key cannot be used to put a long");
+ }
+ }
+ mBundle.putLong(key, value);
+ return this;
+ }
+
+ /**
+ * Put a {@link Rating2} into the metadata. Custom keys may be used, but
+ * if the METADATA_KEYs defined in this class are used they may only be
+ * one of the following:
+ * <ul>
+ * <li>{@link #METADATA_KEY_RATING}</li>
+ * <li>{@link #METADATA_KEY_USER_RATING}</li>
+ * </ul>
+ *
+ * @param key The key for referencing this value
+ * @param value The String value to store
+ * @return The Builder to allow chaining
+ */
+ public Builder putRating(@RatingKey String key, Rating2 value) {
+ if (METADATA_KEYS_TYPE.containsKey(key)) {
+ if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_RATING) {
+ throw new IllegalArgumentException("The " + key
+ + " key cannot be used to put a Rating");
+ }
+ }
+ mBundle.putBundle(key, value.toBundle());
+
+ return this;
+ }
+
+ /**
+ * Put a {@link Bitmap} into the metadata. Custom keys may be used, but
+ * if the METADATA_KEYs defined in this class are used they may only be
+ * one of the following:
+ * <ul>
+ * <li>{@link #METADATA_KEY_ART}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ART}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_ICON}</li>
+ * </ul>
+ * Large bitmaps may be scaled down by the system when
+ * {@link android.media.session.MediaSession#setMetadata} is called.
+ * To pass full resolution images {@link Uri Uris} should be used with
+ * {@link #putString}.
+ *
+ * @param key The key for referencing this value
+ * @param value The Bitmap to store
+ * @return The Builder to allow chaining
+ */
+ public Builder putBitmap(@BitmapKey String key, Bitmap value) {
+ if (METADATA_KEYS_TYPE.containsKey(key)) {
+ if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) {
+ throw new IllegalArgumentException("The " + key
+ + " key cannot be used to put a Bitmap");
+ }
+ }
+ mBundle.putParcelable(key, value);
+ return this;
+ }
+
+ /**
+ * Set an extra {@link Bundle} into the metadata.
+ */
+ public Builder setExtra(Bundle bundle) {
+ mBundle.putBundle(METADATA_KEY_EXTRA, bundle);
+ return this;
+ }
+
+ /**
+ * Creates a {@link MediaMetadata2} instance with the specified fields.
+ *
+ * @return The new MediaMetadata2 instance
+ */
+ public MediaMetadata2 build() {
+ return new MediaMetadata2(mBundle);
+ }
+
+ private Bitmap scaleBitmap(Bitmap bmp, int maxSize) {
+ float maxSizeF = maxSize;
+ float widthScale = maxSizeF / bmp.getWidth();
+ float heightScale = maxSizeF / bmp.getHeight();
+ float scale = Math.min(widthScale, heightScale);
+ int height = (int) (bmp.getHeight() * scale);
+ int width = (int) (bmp.getWidth() * scale);
+ return Bitmap.createScaledBitmap(bmp, width, height, true);
+ }
+ }
+}
+
diff --git a/android/media/MediaPlayer2.java b/android/media/MediaPlayer2.java
new file mode 100644
index 0000000..d36df84
--- /dev/null
+++ b/android/media/MediaPlayer2.java
@@ -0,0 +1,2476 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.PersistableBundle;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.media.MediaDrm;
+import android.media.MediaFormat;
+import android.media.MediaPlayer2Impl;
+import android.media.MediaTimeProvider;
+import android.media.PlaybackParams;
+import android.media.SubtitleController;
+import android.media.SubtitleController.Anchor;
+import android.media.SubtitleData;
+import android.media.SubtitleTrack.RenderingWidget;
+import android.media.SyncParams;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.AutoCloseable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.InetSocketAddress;
+import java.util.concurrent.Executor;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+
+/**
+ * MediaPlayer2 class can be used to control playback
+ * of audio/video files and streams. An example on how to use the methods in
+ * this class can be found in {@link android.widget.VideoView}.
+ *
+ * <p>Topics covered here are:
+ * <ol>
+ * <li><a href="#StateDiagram">State Diagram</a>
+ * <li><a href="#Valid_and_Invalid_States">Valid and Invalid States</a>
+ * <li><a href="#Permissions">Permissions</a>
+ * <li><a href="#Callbacks">Register informational and error callbacks</a>
+ * </ol>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about how to use MediaPlayer2, read the
+ * <a href="{@docRoot}guide/topics/media/mediaplayer.html">Media Playback</a> developer guide.</p>
+ * </div>
+ *
+ * <a name="StateDiagram"></a>
+ * <h3>State Diagram</h3>
+ *
+ * <p>Playback control of audio/video files and streams is managed as a state
+ * machine. The following diagram shows the life cycle and the states of a
+ * MediaPlayer2 object driven by the supported playback control operations.
+ * The ovals represent the states a MediaPlayer2 object may reside
+ * in. The arcs represent the playback control operations that drive the object
+ * state transition. There are two types of arcs. The arcs with a single arrow
+ * head represent synchronous method calls, while those with
+ * a double arrow head represent asynchronous method calls.</p>
+ *
+ * <p><img src="../../../images/mediaplayer_state_diagram.gif"
+ * alt="MediaPlayer State diagram"
+ * border="0" /></p>
+ *
+ * <p>From this state diagram, one can see that a MediaPlayer2 object has the
+ * following states:</p>
+ * <ul>
+ * <li>When a MediaPlayer2 object is just created using <code>new</code> or
+ * after {@link #reset()} is called, it is in the <em>Idle</em> state; and after
+ * {@link #close()} is called, it is in the <em>End</em> state. Between these
+ * two states is the life cycle of the MediaPlayer2 object.
+ * <ul>
+ * <li>There is a subtle but important difference between a newly constructed
+ * MediaPlayer2 object and the MediaPlayer2 object after {@link #reset()}
+ * is called. It is a programming error to invoke methods such
+ * as {@link #getCurrentPosition()},
+ * {@link #getDuration()}, {@link #getVideoHeight()},
+ * {@link #getVideoWidth()}, {@link #setAudioAttributes(AudioAttributes)},
+ * {@link #setVolume(float, float)}, {@link #pause()}, {@link #play()},
+ * {@link #seekTo(long, int)} or
+ * {@link #prepareAsync()} in the <em>Idle</em> state for both cases. If any of these
+ * methods is called right after a MediaPlayer2 object is constructed,
+ * the user supplied callback method OnErrorListener.onError() won't be
+ * called by the internal player engine and the object state remains
+ * unchanged; but if these methods are called right after {@link #reset()},
+ * the user supplied callback method OnErrorListener.onError() will be
+ * invoked by the internal player engine and the object will be
+ * transfered to the <em>Error</em> state. </li>
+ * <li>It is also recommended that once
+ * a MediaPlayer2 object is no longer being used, call {@link #close()} immediately
+ * so that resources used by the internal player engine associated with the
+ * MediaPlayer2 object can be released immediately. Resource may include
+ * singleton resources such as hardware acceleration components and
+ * failure to call {@link #close()} may cause subsequent instances of
+ * MediaPlayer2 objects to fallback to software implementations or fail
+ * altogether. Once the MediaPlayer2
+ * object is in the <em>End</em> state, it can no longer be used and
+ * there is no way to bring it back to any other state. </li>
+ * <li>Furthermore,
+ * the MediaPlayer2 objects created using <code>new</code> is in the
+ * <em>Idle</em> state.
+ * </li>
+ * </ul>
+ * </li>
+ * <li>In general, some playback control operation may fail due to various
+ * reasons, such as unsupported audio/video format, poorly interleaved
+ * audio/video, resolution too high, streaming timeout, and the like.
+ * Thus, error reporting and recovery is an important concern under
+ * these circumstances. Sometimes, due to programming errors, invoking a playback
+ * control operation in an invalid state may also occur. Under all these
+ * error conditions, the internal player engine invokes a user supplied
+ * EventCallback.onError() method if an EventCallback has been
+ * registered beforehand via
+ * {@link #registerEventCallback(Executor, EventCallback)}.
+ * <ul>
+ * <li>It is important to note that once an error occurs, the
+ * MediaPlayer2 object enters the <em>Error</em> state (except as noted
+ * above), even if an error listener has not been registered by the application.</li>
+ * <li>In order to reuse a MediaPlayer2 object that is in the <em>
+ * Error</em> state and recover from the error,
+ * {@link #reset()} can be called to restore the object to its <em>Idle</em>
+ * state.</li>
+ * <li>It is good programming practice to have your application
+ * register a OnErrorListener to look out for error notifications from
+ * the internal player engine.</li>
+ * <li>IllegalStateException is
+ * thrown to prevent programming errors such as calling
+ * {@link #prepareAsync()}, {@link #setDataSource(DataSourceDesc)}, or
+ * {@code setPlaylist} methods in an invalid state. </li>
+ * </ul>
+ * </li>
+ * <li>Calling
+ * {@link #setDataSource(DataSourceDesc)}, or
+ * {@code setPlaylist} transfers a
+ * MediaPlayer2 object in the <em>Idle</em> state to the
+ * <em>Initialized</em> state.
+ * <ul>
+ * <li>An IllegalStateException is thrown if
+ * setDataSource() or setPlaylist() is called in any other state.</li>
+ * <li>It is good programming
+ * practice to always look out for <code>IllegalArgumentException</code>
+ * and <code>IOException</code> that may be thrown from
+ * <code>setDataSource</code> and <code>setPlaylist</code> methods.</li>
+ * </ul>
+ * </li>
+ * <li>A MediaPlayer2 object must first enter the <em>Prepared</em> state
+ * before playback can be started.
+ * <ul>
+ * <li>There are an asynchronous way that the <em>Prepared</em> state can be reached:
+ * a call to {@link #prepareAsync()} (asynchronous) which
+ * first transfers the object to the <em>Preparing</em> state after the
+ * call returns (which occurs almost right way) while the internal
+ * player engine continues working on the rest of preparation work
+ * until the preparation work completes. When the preparation completes,
+ * the internal player engine then calls a user supplied callback method,
+ * onInfo() of the EventCallback interface with {@link #MEDIA_INFO_PREPARED}, if an
+ * EventCallback is registered beforehand via
+ * {@link #registerEventCallback(Executor, EventCallback)}.</li>
+ * <li>It is important to note that
+ * the <em>Preparing</em> state is a transient state, and the behavior
+ * of calling any method with side effect while a MediaPlayer2 object is
+ * in the <em>Preparing</em> state is undefined.</li>
+ * <li>An IllegalStateException is
+ * thrown if {@link #prepareAsync()} is called in
+ * any other state.</li>
+ * <li>While in the <em>Prepared</em> state, properties
+ * such as audio/sound volume, screenOnWhilePlaying, looping can be
+ * adjusted by invoking the corresponding set methods.</li>
+ * </ul>
+ * </li>
+ * <li>To start the playback, {@link #play()} must be called. After
+ * {@link #play()} returns successfully, the MediaPlayer2 object is in the
+ * <em>Started</em> state. {@link #isPlaying()} can be called to test
+ * whether the MediaPlayer2 object is in the <em>Started</em> state.
+ * <ul>
+ * <li>While in the <em>Started</em> state, the internal player engine calls
+ * a user supplied EventCallback.onBufferingUpdate() callback
+ * method if an EventCallback has been registered beforehand
+ * via {@link #registerEventCallback(Executor, EventCallback)}.
+ * This callback allows applications to keep track of the buffering status
+ * while streaming audio/video.</li>
+ * <li>Calling {@link #play()} has not effect
+ * on a MediaPlayer2 object that is already in the <em>Started</em> state.</li>
+ * </ul>
+ * </li>
+ * <li>Playback can be paused and stopped, and the current playback position
+ * can be adjusted. Playback can be paused via {@link #pause()}. When the call to
+ * {@link #pause()} returns, the MediaPlayer2 object enters the
+ * <em>Paused</em> state. Note that the transition from the <em>Started</em>
+ * state to the <em>Paused</em> state and vice versa happens
+ * asynchronously in the player engine. It may take some time before
+ * the state is updated in calls to {@link #isPlaying()}, and it can be
+ * a number of seconds in the case of streamed content.
+ * <ul>
+ * <li>Calling {@link #play()} to resume playback for a paused
+ * MediaPlayer2 object, and the resumed playback
+ * position is the same as where it was paused. When the call to
+ * {@link #play()} returns, the paused MediaPlayer2 object goes back to
+ * the <em>Started</em> state.</li>
+ * <li>Calling {@link #pause()} has no effect on
+ * a MediaPlayer2 object that is already in the <em>Paused</em> state.</li>
+ * </ul>
+ * </li>
+ * <li>The playback position can be adjusted with a call to
+ * {@link #seekTo(long, int)}.
+ * <ul>
+ * <li>Although the asynchronuous {@link #seekTo(long, int)}
+ * call returns right away, the actual seek operation may take a while to
+ * finish, especially for audio/video being streamed. When the actual
+ * seek operation completes, the internal player engine calls a user
+ * supplied EventCallback.onInfo() with {@link #MEDIA_INFO_COMPLETE_CALL_SEEK}
+ * if an EventCallback has been registered beforehand via
+ * {@link #registerEventCallback(Executor, EventCallback)}.</li>
+ * <li>Please
+ * note that {@link #seekTo(long, int)} can also be called in the other states,
+ * such as <em>Prepared</em>, <em>Paused</em> and <em>PlaybackCompleted
+ * </em> state. When {@link #seekTo(long, int)} is called in those states,
+ * one video frame will be displayed if the stream has video and the requested
+ * position is valid.
+ * </li>
+ * <li>Furthermore, the actual current playback position
+ * can be retrieved with a call to {@link #getCurrentPosition()}, which
+ * is helpful for applications such as a Music player that need to keep
+ * track of the playback progress.</li>
+ * </ul>
+ * </li>
+ * <li>When the playback reaches the end of stream, the playback completes.
+ * <ul>
+ * <li>If the looping mode was being set to one of the values of
+ * {@link #LOOPING_MODE_FULL}, {@link #LOOPING_MODE_SINGLE} or
+ * {@link #LOOPING_MODE_SHUFFLE} with
+ * {@link #setLoopingMode(int)}, the MediaPlayer2 object shall remain in
+ * the <em>Started</em> state.</li>
+ * <li>If the looping mode was set to <var>false
+ * </var>, the player engine calls a user supplied callback method,
+ * EventCallback.onCompletion(), if an EventCallback is registered
+ * beforehand via {@link #registerEventCallback(Executor, EventCallback)}.
+ * The invoke of the callback signals that the object is now in the <em>
+ * PlaybackCompleted</em> state.</li>
+ * <li>While in the <em>PlaybackCompleted</em>
+ * state, calling {@link #play()} can restart the playback from the
+ * beginning of the audio/video source.</li>
+ * </ul>
+ *
+ *
+ * <a name="Valid_and_Invalid_States"></a>
+ * <h3>Valid and invalid states</h3>
+ *
+ * <table border="0" cellspacing="0" cellpadding="0">
+ * <tr><td>Method Name </p></td>
+ * <td>Valid Sates </p></td>
+ * <td>Invalid States </p></td>
+ * <td>Comments </p></td></tr>
+ * <tr><td>attachAuxEffect </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
+ * <td>{Idle, Error} </p></td>
+ * <td>This method must be called after setDataSource or setPlaylist.
+ * Calling it does not change the object state. </p></td></tr>
+ * <tr><td>getAudioSessionId </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>getCurrentPosition </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted} </p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change the
+ * state. Calling this method in an invalid state transfers the object
+ * to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getDuration </p></td>
+ * <td>{Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
+ * <td>{Idle, Initialized, Error} </p></td>
+ * <td>Successful invoke of this method in a valid state does not change the
+ * state. Calling this method in an invalid state transfers the object
+ * to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getVideoHeight </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change the
+ * state. Calling this method in an invalid state transfers the object
+ * to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getVideoWidth </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an invalid state transfers the
+ * object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>isPlaying </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an invalid state transfers the
+ * object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>pause </p></td>
+ * <td>{Started, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Prepared, Stopped, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Paused</em> state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>prepareAsync </p></td>
+ * <td>{Initialized, Stopped} </p></td>
+ * <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Preparing</em> state. Calling this method in an
+ * invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>release </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>After {@link #close()}, the object is no longer available. </p></td></tr>
+ * <tr><td>reset </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted, Error}</p></td>
+ * <td>{}</p></td>
+ * <td>After {@link #reset()}, the object is like being just created.</p></td></tr>
+ * <tr><td>seekTo </p></td>
+ * <td>{Prepared, Started, Paused, PlaybackCompleted} </p></td>
+ * <td>{Idle, Initialized, Stopped, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an invalid state transfers the
+ * object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>setAudioAttributes </p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method does not change the state. In order for the
+ * target audio attributes type to become effective, this method must be called before
+ * prepareAsync().</p></td></tr>
+ * <tr><td>setAudioSessionId </p></td>
+ * <td>{Idle} </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
+ * Error} </p></td>
+ * <td>This method must be called in idle state as the audio session ID must be known before
+ * calling setDataSource or setPlaylist. Calling it does not change the object
+ * state. </p></td></tr>
+ * <tr><td>setAudioStreamType (deprecated)</p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method does not change the state. In order for the
+ * target audio stream type to become effective, this method must be called before
+ * prepareAsync().</p></td></tr>
+ * <tr><td>setAuxEffectSendLevel </p></td>
+ * <td>any</p></td>
+ * <td>{} </p></td>
+ * <td>Calling this method does not change the object state. </p></td></tr>
+ * <tr><td>setDataSource </p></td>
+ * <td>{Idle} </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
+ * Error} </p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Initialized</em> state. Calling this method in an
+ * invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>setPlaylist </p></td>
+ * <td>{Idle} </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
+ * Error} </p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Initialized</em> state. Calling this method in an
+ * invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>setDisplay </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setSurface </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setLoopingMode </p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>isLooping </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>registerDrmEventCallback </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>registerEventCallback </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setPlaybackParams</p></td>
+ * <td>{Initialized, Prepared, Started, Paused, PlaybackCompleted, Error}</p></td>
+ * <td>{Idle, Stopped} </p></td>
+ * <td>This method will change state in some cases, depending on when it's called.
+ * </p></td></tr>
+ * <tr><td>setVolume </p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.
+ * <tr><td>play </p></td>
+ * <td>{Prepared, Started, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Stopped, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Started</em> state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>stop </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Stopped</em> state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>getTrackInfo </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ * <tr><td>selectTrack </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ * <tr><td>deselectTrack </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ *
+ * </table>
+ *
+ * <a name="Permissions"></a>
+ * <h3>Permissions</h3>
+ * <p>One may need to declare a corresponding WAKE_LOCK permission {@link
+ * android.R.styleable#AndroidManifestUsesPermission <uses-permission>}
+ * element.
+ *
+ * <p>This class requires the {@link android.Manifest.permission#INTERNET} permission
+ * when used with network-based content.
+ *
+ * <a name="Callbacks"></a>
+ * <h3>Callbacks</h3>
+ * <p>Applications may want to register for informational and error
+ * events in order to be informed of some internal state update and
+ * possible runtime errors during playback or streaming. Registration for
+ * these events is done by properly setting the appropriate listeners (via calls
+ * to
+ * {@link #registerEventCallback(Executor, EventCallback)},
+ * {@link #registerDrmEventCallback(Executor, DrmEventCallback)}).
+ * In order to receive the respective callback
+ * associated with these listeners, applications are required to create
+ * MediaPlayer2 objects on a thread with its own Looper running (main UI
+ * thread by default has a Looper running).
+ *
+ */
+public abstract class MediaPlayer2 implements SubtitleController.Listener
+ , AudioRouting
+ , AutoCloseable
+{
+ /**
+ Constant to retrieve only the new metadata since the last
+ call.
+ // FIXME: unhide.
+ // FIXME: add link to getMetadata(boolean, boolean)
+ {@hide}
+ */
+ public static final boolean METADATA_UPDATE_ONLY = true;
+
+ /**
+ Constant to retrieve all the metadata.
+ // FIXME: unhide.
+ // FIXME: add link to getMetadata(boolean, boolean)
+ {@hide}
+ */
+ public static final boolean METADATA_ALL = false;
+
+ /**
+ Constant to enable the metadata filter during retrieval.
+ // FIXME: unhide.
+ // FIXME: add link to getMetadata(boolean, boolean)
+ {@hide}
+ */
+ public static final boolean APPLY_METADATA_FILTER = true;
+
+ /**
+ Constant to disable the metadata filter during retrieval.
+ // FIXME: unhide.
+ // FIXME: add link to getMetadata(boolean, boolean)
+ {@hide}
+ */
+ public static final boolean BYPASS_METADATA_FILTER = false;
+
+ /**
+ * Create a MediaPlayer2 object.
+ *
+ * @return A MediaPlayer2 object created
+ */
+ public static final MediaPlayer2 create() {
+ // TODO: load MediaUpdate APK
+ return new MediaPlayer2Impl();
+ }
+
+ /**
+ * @hide
+ */
+ // add hidden empty constructor so it doesn't show in SDK
+ public MediaPlayer2() { }
+
+ /**
+ * Create a request parcel which can be routed to the native media
+ * player using {@link #invoke(Parcel, Parcel)}. The Parcel
+ * returned has the proper InterfaceToken set. The caller should
+ * not overwrite that token, i.e it can only append data to the
+ * Parcel.
+ *
+ * @return A parcel suitable to hold a request for the native
+ * player.
+ * {@hide}
+ */
+ public Parcel newRequest() {
+ return null;
+ }
+
+ /**
+ * Invoke a generic method on the native player using opaque
+ * parcels for the request and reply. Both payloads' format is a
+ * convention between the java caller and the native player.
+ * Must be called after setDataSource or setPlaylist to make sure a native player
+ * exists. On failure, a RuntimeException is thrown.
+ *
+ * @param request Parcel with the data for the extension. The
+ * caller must use {@link #newRequest()} to get one.
+ *
+ * @param reply Output parcel with the data returned by the
+ * native player.
+ * {@hide}
+ */
+ public void invoke(Parcel request, Parcel reply) { }
+
+ /**
+ * Sets the {@link SurfaceHolder} to use for displaying the video
+ * portion of the media.
+ *
+ * Either a surface holder or surface must be set if a display or video sink
+ * is needed. Not calling this method or {@link #setSurface(Surface)}
+ * when playing back a video will result in only the audio track being played.
+ * A null surface holder or surface will result in only the audio track being
+ * played.
+ *
+ * @param sh the SurfaceHolder to use for video display
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ * @hide
+ */
+ public abstract void setDisplay(SurfaceHolder sh);
+
+ /**
+ * Sets the {@link Surface} to be used as the sink for the video portion of
+ * the media. Setting a
+ * Surface will un-set any Surface or SurfaceHolder that was previously set.
+ * A null surface will result in only the audio track being played.
+ *
+ * If the Surface sends frames to a {@link SurfaceTexture}, the timestamps
+ * returned from {@link SurfaceTexture#getTimestamp()} will have an
+ * unspecified zero point. These timestamps cannot be directly compared
+ * between different media sources, different instances of the same media
+ * source, or multiple runs of the same program. The timestamp is normally
+ * monotonically increasing and is unaffected by time-of-day adjustments,
+ * but it is reset when the position is set.
+ *
+ * @param surface The {@link Surface} to be used for the video portion of
+ * the media.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ */
+ public abstract void setSurface(Surface surface);
+
+ /* Do not change these video scaling mode values below without updating
+ * their counterparts in system/window.h! Please do not forget to update
+ * {@link #isVideoScalingModeSupported} when new video scaling modes
+ * are added.
+ */
+ /**
+ * Specifies a video scaling mode. The content is stretched to the
+ * surface rendering area. When the surface has the same aspect ratio
+ * as the content, the aspect ratio of the content is maintained;
+ * otherwise, the aspect ratio of the content is not maintained when video
+ * is being rendered.
+ * There is no content cropping with this video scaling mode.
+ */
+ public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT = 1;
+
+ /**
+ * Specifies a video scaling mode. The content is scaled, maintaining
+ * its aspect ratio. The whole surface area is always used. When the
+ * aspect ratio of the content is the same as the surface, no content
+ * is cropped; otherwise, content is cropped to fit the surface.
+ * @hide
+ */
+ public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING = 2;
+
+ /**
+ * Sets video scaling mode. To make the target video scaling mode
+ * effective during playback, this method must be called after
+ * data source is set. If not called, the default video
+ * scaling mode is {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT}.
+ *
+ * <p> The supported video scaling modes are:
+ * <ul>
+ * <li> {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT}
+ * </ul>
+ *
+ * @param mode target video scaling mode. Must be one of the supported
+ * video scaling modes; otherwise, IllegalArgumentException will be thrown.
+ *
+ * @see MediaPlayer2#VIDEO_SCALING_MODE_SCALE_TO_FIT
+ * @hide
+ */
+ public void setVideoScalingMode(int mode) { }
+
+ /**
+ * Discards all pending commands.
+ */
+ public abstract void clearPendingCommands();
+
+ /**
+ * Sets the data source as described by a DataSourceDesc.
+ *
+ * @param dsd the descriptor of data source you want to play
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws NullPointerException if dsd is null
+ */
+ public abstract void setDataSource(@NonNull DataSourceDesc dsd) throws IOException;
+
+ /**
+ * Gets the current data source as described by a DataSourceDesc.
+ *
+ * @return the current DataSourceDesc
+ */
+ public abstract DataSourceDesc getCurrentDataSource();
+
+ /**
+ * Sets the play list.
+ *
+ * If startIndex falls outside play list range, it will be clamped to the nearest index
+ * in the play list.
+ *
+ * @param pl the play list of data source you want to play
+ * @param startIndex the index of the DataSourceDesc in the play list you want to play first
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if pl is null or empty, or pl contains null DataSourceDesc
+ */
+ public abstract void setPlaylist(@NonNull List<DataSourceDesc> pl, int startIndex)
+ throws IOException;
+
+ /**
+ * Gets a copy of the play list.
+ *
+ * @return a copy of the play list used by {@link MediaPlayer2}
+ */
+ public abstract List<DataSourceDesc> getPlaylist();
+
+ /**
+ * Sets the index of current DataSourceDesc in the play list to be played.
+ *
+ * @param index the index of DataSourceDesc in the play list you want to play
+ * @throws IllegalArgumentException if the play list is null
+ * @throws NullPointerException if index is outside play list range
+ */
+ public abstract void setCurrentPlaylistItem(int index);
+
+ /**
+ * Sets the index of next-to-be-played DataSourceDesc in the play list.
+ *
+ * @param index the index of next-to-be-played DataSourceDesc in the play list
+ * @throws IllegalArgumentException if the play list is null
+ * @throws NullPointerException if index is outside play list range
+ */
+ public abstract void setNextPlaylistItem(int index);
+
+ /**
+ * Gets the current index of play list.
+ *
+ * @return the index of the current DataSourceDesc in the play list
+ */
+ public abstract int getCurrentPlaylistItemIndex();
+
+ /**
+ * Specifies a playback looping mode. The source will not be played in looping mode.
+ */
+ public static final int LOOPING_MODE_NONE = 0;
+ /**
+ * Specifies a playback looping mode. The full list of source will be played in looping mode,
+ * and in the order specified in the play list.
+ */
+ public static final int LOOPING_MODE_FULL = 1;
+ /**
+ * Specifies a playback looping mode. The current DataSourceDesc will be played in looping mode.
+ */
+ public static final int LOOPING_MODE_SINGLE = 2;
+ /**
+ * Specifies a playback looping mode. The full list of source will be played in looping mode,
+ * and in a random order.
+ */
+ public static final int LOOPING_MODE_SHUFFLE = 3;
+
+ /** @hide */
+ @IntDef(
+ value = {
+ LOOPING_MODE_NONE,
+ LOOPING_MODE_FULL,
+ LOOPING_MODE_SINGLE,
+ LOOPING_MODE_SHUFFLE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LoopingMode {}
+
+ /**
+ * Sets the looping mode of the play list.
+ * The mode shall be one of {@link #LOOPING_MODE_NONE}, {@link #LOOPING_MODE_FULL},
+ * {@link #LOOPING_MODE_SINGLE}, {@link #LOOPING_MODE_SHUFFLE}.
+ *
+ * @param mode the mode in which the play list will be played
+ * @throws IllegalArgumentException if mode is not supported
+ */
+ public abstract void setLoopingMode(@LoopingMode int mode);
+
+ /**
+ * Gets the looping mode of play list.
+ *
+ * @return the looping mode of the play list
+ */
+ public abstract int getLoopingMode();
+
+ /**
+ * Moves the DataSourceDesc at indexFrom in the play list to indexTo.
+ *
+ * @throws IllegalArgumentException if the play list is null
+ * @throws IndexOutOfBoundsException if indexFrom or indexTo is outside play list range
+ */
+ public abstract void movePlaylistItem(int indexFrom, int indexTo);
+
+ /**
+ * Removes the DataSourceDesc at index in the play list.
+ *
+ * If index is same as the current index of the play list, current DataSourceDesc
+ * will be stopped and playback moves to next source in the list.
+ *
+ * @return the removed DataSourceDesc at index in the play list
+ * @throws IllegalArgumentException if the play list is null
+ * @throws IndexOutOfBoundsException if index is outside play list range
+ */
+ public abstract DataSourceDesc removePlaylistItem(int index);
+
+ /**
+ * Inserts the DataSourceDesc to the play list at position index.
+ *
+ * This will not change the DataSourceDesc currently being played.
+ * If index is less than or equal to the current index of the play list,
+ * the current index of the play list will be incremented correspondingly.
+ *
+ * @param index the index you want to add dsd to the play list
+ * @param dsd the descriptor of data source you want to add to the play list
+ * @throws IndexOutOfBoundsException if index is outside play list range
+ * @throws NullPointerException if dsd is null
+ */
+ public abstract void addPlaylistItem(int index, DataSourceDesc dsd);
+
+ /**
+ * replaces the DataSourceDesc at index in the play list with given dsd.
+ *
+ * When index is same as the current index of the play list, the current source
+ * will be stopped and the new source will be played, except that if new
+ * and old source only differ on end position and current media position is
+ * smaller then the new end position.
+ *
+ * This will not change the DataSourceDesc currently being played.
+ * If index is less than or equal to the current index of the play list,
+ * the current index of the play list will be incremented correspondingly.
+ *
+ * @param index the index you want to add dsd to the play list
+ * @param dsd the descriptor of data source you want to add to the play list
+ * @throws IndexOutOfBoundsException if index is outside play list range
+ * @throws NullPointerException if dsd is null
+ */
+ public abstract DataSourceDesc editPlaylistItem(int index, DataSourceDesc dsd);
+
+ /**
+ * Prepares the player for playback, synchronously.
+ *
+ * After setting the datasource and the display surface, you need to either
+ * call prepare() or prepareAsync(). For files, it is OK to call prepare(),
+ * which blocks until MediaPlayer2 is ready for playback.
+ *
+ * @throws IOException if source can not be accessed
+ * @throws IllegalStateException if it is called in an invalid state
+ * @hide
+ */
+ public void prepare() throws IOException { }
+
+ /**
+ * Prepares the player for playback, asynchronously.
+ *
+ * After setting the datasource and the display surface, you need to
+ * call prepareAsync().
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ public abstract void prepareAsync();
+
+ /**
+ * Starts or resumes playback. If playback had previously been paused,
+ * playback will continue from where it was paused. If playback had
+ * been stopped, or never started before, playback will start at the
+ * beginning.
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ public abstract void play();
+
+ /**
+ * Stops playback after playback has been started or paused.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ * @hide
+ */
+ public void stop() { }
+
+ /**
+ * Pauses playback. Call play() to resume.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ public abstract void pause();
+
+ //--------------------------------------------------------------------------
+ // Explicit Routing
+ //--------------------
+
+ /**
+ * Specifies an audio device (via an {@link AudioDeviceInfo} object) to route
+ * the output from this MediaPlayer2.
+ * @param deviceInfo The {@link AudioDeviceInfo} specifying the audio sink or source.
+ * If deviceInfo is null, default routing is restored.
+ * @return true if succesful, false if the specified {@link AudioDeviceInfo} is non-null and
+ * does not correspond to a valid audio device.
+ */
+ @Override
+ public abstract boolean setPreferredDevice(AudioDeviceInfo deviceInfo);
+
+ /**
+ * Returns the selected output specified by {@link #setPreferredDevice}. Note that this
+ * is not guaranteed to correspond to the actual device being used for playback.
+ */
+ @Override
+ public abstract AudioDeviceInfo getPreferredDevice();
+
+ /**
+ * Returns an {@link AudioDeviceInfo} identifying the current routing of this MediaPlayer2
+ * Note: The query is only valid if the MediaPlayer2 is currently playing.
+ * If the player is not playing, the returned device can be null or correspond to previously
+ * selected device when the player was last active.
+ */
+ @Override
+ public abstract AudioDeviceInfo getRoutedDevice();
+
+ /**
+ * Adds an {@link AudioRouting.OnRoutingChangedListener} to receive notifications of routing
+ * changes on this MediaPlayer2.
+ * @param listener The {@link AudioRouting.OnRoutingChangedListener} interface to receive
+ * notifications of rerouting events.
+ * @param handler Specifies the {@link Handler} object for the thread on which to execute
+ * the callback. If <code>null</code>, the handler on the main looper will be used.
+ */
+ @Override
+ public abstract void addOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener,
+ Handler handler);
+
+ /**
+ * Removes an {@link AudioRouting.OnRoutingChangedListener} which has been previously added
+ * to receive rerouting notifications.
+ * @param listener The previously added {@link AudioRouting.OnRoutingChangedListener} interface
+ * to remove.
+ */
+ @Override
+ public abstract void removeOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener);
+
+ /**
+ * Set the low-level power management behavior for this MediaPlayer2.
+ *
+ * <p>This function has the MediaPlayer2 access the low-level power manager
+ * service to control the device's power usage while playing is occurring.
+ * The parameter is a combination of {@link android.os.PowerManager} wake flags.
+ * Use of this method requires {@link android.Manifest.permission#WAKE_LOCK}
+ * permission.
+ * By default, no attempt is made to keep the device awake during playback.
+ *
+ * @param context the Context to use
+ * @param mode the power/wake mode to set
+ * @see android.os.PowerManager
+ * @hide
+ */
+ public abstract void setWakeMode(Context context, int mode);
+
+ /**
+ * Control whether we should use the attached SurfaceHolder to keep the
+ * screen on while video playback is occurring. This is the preferred
+ * method over {@link #setWakeMode} where possible, since it doesn't
+ * require that the application have permission for low-level wake lock
+ * access.
+ *
+ * @param screenOn Supply true to keep the screen on, false to allow it
+ * to turn off.
+ * @hide
+ */
+ public abstract void setScreenOnWhilePlaying(boolean screenOn);
+
+ /**
+ * Returns the width of the video.
+ *
+ * @return the width of the video, or 0 if there is no video,
+ * no display surface was set, or the width has not been determined
+ * yet. The {@code EventCallback} can be registered via
+ * {@link #registerEventCallback(Executor, EventCallback)} to provide a
+ * notification {@code EventCallback.onVideoSizeChanged} when the width is available.
+ */
+ public abstract int getVideoWidth();
+
+ /**
+ * Returns the height of the video.
+ *
+ * @return the height of the video, or 0 if there is no video,
+ * no display surface was set, or the height has not been determined
+ * yet. The {@code EventCallback} can be registered via
+ * {@link #registerEventCallback(Executor, EventCallback)} to provide a
+ * notification {@code EventCallback.onVideoSizeChanged} when the height is available.
+ */
+ public abstract int getVideoHeight();
+
+ /**
+ * Return Metrics data about the current player.
+ *
+ * @return a {@link PersistableBundle} containing the set of attributes and values
+ * available for the media being handled by this instance of MediaPlayer2
+ * The attributes are descibed in {@link MetricsConstants}.
+ *
+ * Additional vendor-specific fields may also be present in
+ * the return value.
+ */
+ public abstract PersistableBundle getMetrics();
+
+ /**
+ * Checks whether the MediaPlayer2 is playing.
+ *
+ * @return true if currently playing, false otherwise
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ */
+ public abstract boolean isPlaying();
+
+ /**
+ * Gets the current buffering management params used by the source component.
+ * Calling it only after {@code setDataSource} has been called.
+ * Each type of data source might have different set of default params.
+ *
+ * @return the current buffering management params used by the source component.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized, or {@code setDataSource} has not been called.
+ * @hide
+ */
+ @NonNull
+ public BufferingParams getBufferingParams() {
+ return new BufferingParams.Builder().build();
+ }
+
+ /**
+ * Sets buffering management params.
+ * The object sets its internal BufferingParams to the input, except that the input is
+ * invalid or not supported.
+ * Call it only after {@code setDataSource} has been called.
+ * The input is a hint to MediaPlayer2.
+ *
+ * @param params the buffering management params.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released, or {@code setDataSource} has not been called.
+ * @throws IllegalArgumentException if params is invalid or not supported.
+ * @hide
+ */
+ public void setBufferingParams(@NonNull BufferingParams params) { }
+
+ /**
+ * Change playback speed of audio by resampling the audio.
+ * <p>
+ * Specifies resampling as audio mode for variable rate playback, i.e.,
+ * resample the waveform based on the requested playback rate to get
+ * a new waveform, and play back the new waveform at the original sampling
+ * frequency.
+ * When rate is larger than 1.0, pitch becomes higher.
+ * When rate is smaller than 1.0, pitch becomes lower.
+ *
+ * @hide
+ */
+ public static final int PLAYBACK_RATE_AUDIO_MODE_RESAMPLE = 2;
+
+ /**
+ * Change playback speed of audio without changing its pitch.
+ * <p>
+ * Specifies time stretching as audio mode for variable rate playback.
+ * Time stretching changes the duration of the audio samples without
+ * affecting its pitch.
+ * <p>
+ * This mode is only supported for a limited range of playback speed factors,
+ * e.g. between 1/2x and 2x.
+ *
+ * @hide
+ */
+ public static final int PLAYBACK_RATE_AUDIO_MODE_STRETCH = 1;
+
+ /**
+ * Change playback speed of audio without changing its pitch, and
+ * possibly mute audio if time stretching is not supported for the playback
+ * speed.
+ * <p>
+ * Try to keep audio pitch when changing the playback rate, but allow the
+ * system to determine how to change audio playback if the rate is out
+ * of range.
+ *
+ * @hide
+ */
+ public static final int PLAYBACK_RATE_AUDIO_MODE_DEFAULT = 0;
+
+ /** @hide */
+ @IntDef(
+ value = {
+ PLAYBACK_RATE_AUDIO_MODE_DEFAULT,
+ PLAYBACK_RATE_AUDIO_MODE_STRETCH,
+ PLAYBACK_RATE_AUDIO_MODE_RESAMPLE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PlaybackRateAudioMode {}
+
+ /**
+ * Sets playback rate and audio mode.
+ *
+ * @param rate the ratio between desired playback rate and normal one.
+ * @param audioMode audio playback mode. Must be one of the supported
+ * audio modes.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ * @throws IllegalArgumentException if audioMode is not supported.
+ *
+ * @hide
+ */
+ @NonNull
+ public PlaybackParams easyPlaybackParams(float rate, @PlaybackRateAudioMode int audioMode) {
+ return new PlaybackParams();
+ }
+
+ /**
+ * Sets playback rate using {@link PlaybackParams}. The object sets its internal
+ * PlaybackParams to the input, except that the object remembers previous speed
+ * when input speed is zero. This allows the object to resume at previous speed
+ * when play() is called. Calling it before the object is prepared does not change
+ * the object state. After the object is prepared, calling it with zero speed is
+ * equivalent to calling pause(). After the object is prepared, calling it with
+ * non-zero speed is equivalent to calling play().
+ *
+ * @param params the playback params.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ * @throws IllegalArgumentException if params is not supported.
+ */
+ public abstract void setPlaybackParams(@NonNull PlaybackParams params);
+
+ /**
+ * Gets the playback params, containing the current playback rate.
+ *
+ * @return the playback params.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ @NonNull
+ public abstract PlaybackParams getPlaybackParams();
+
+ /**
+ * Sets A/V sync mode.
+ *
+ * @param params the A/V sync params to apply
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ * @throws IllegalArgumentException if params are not supported.
+ */
+ public abstract void setSyncParams(@NonNull SyncParams params);
+
+ /**
+ * Gets the A/V sync mode.
+ *
+ * @return the A/V sync params
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ @NonNull
+ public abstract SyncParams getSyncParams();
+
+ /**
+ * Seek modes used in method seekTo(long, int) to move media position
+ * to a specified location.
+ *
+ * Do not change these mode values without updating their counterparts
+ * in include/media/IMediaSource.h!
+ */
+ /**
+ * This mode is used with {@link #seekTo(long, int)} to move media position to
+ * a sync (or key) frame associated with a data source that is located
+ * right before or at the given time.
+ *
+ * @see #seekTo(long, int)
+ */
+ public static final int SEEK_PREVIOUS_SYNC = 0x00;
+ /**
+ * This mode is used with {@link #seekTo(long, int)} to move media position to
+ * a sync (or key) frame associated with a data source that is located
+ * right after or at the given time.
+ *
+ * @see #seekTo(long, int)
+ */
+ public static final int SEEK_NEXT_SYNC = 0x01;
+ /**
+ * This mode is used with {@link #seekTo(long, int)} to move media position to
+ * a sync (or key) frame associated with a data source that is located
+ * closest to (in time) or at the given time.
+ *
+ * @see #seekTo(long, int)
+ */
+ public static final int SEEK_CLOSEST_SYNC = 0x02;
+ /**
+ * This mode is used with {@link #seekTo(long, int)} to move media position to
+ * a frame (not necessarily a key frame) associated with a data source that
+ * is located closest to or at the given time.
+ *
+ * @see #seekTo(long, int)
+ */
+ public static final int SEEK_CLOSEST = 0x03;
+
+ /** @hide */
+ @IntDef(
+ value = {
+ SEEK_PREVIOUS_SYNC,
+ SEEK_NEXT_SYNC,
+ SEEK_CLOSEST_SYNC,
+ SEEK_CLOSEST,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SeekMode {}
+
+ /**
+ * Moves the media to specified time position by considering the given mode.
+ * <p>
+ * When seekTo is finished, the user will be notified via OnSeekComplete supplied by the user.
+ * There is at most one active seekTo processed at any time. If there is a to-be-completed
+ * seekTo, new seekTo requests will be queued in such a way that only the last request
+ * is kept. When current seekTo is completed, the queued request will be processed if
+ * that request is different from just-finished seekTo operation, i.e., the requested
+ * position or mode is different.
+ *
+ * @param msec the offset in milliseconds from the start to seek to.
+ * When seeking to the given time position, there is no guarantee that the data source
+ * has a frame located at the position. When this happens, a frame nearby will be rendered.
+ * If msec is negative, time position zero will be used.
+ * If msec is larger than duration, duration will be used.
+ * @param mode the mode indicating where exactly to seek to.
+ * Use {@link #SEEK_PREVIOUS_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp earlier than or the same as msec. Use
+ * {@link #SEEK_NEXT_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp later than or the same as msec. Use
+ * {@link #SEEK_CLOSEST_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp closest to or the same as msec. Use
+ * {@link #SEEK_CLOSEST} if one wants to seek to a frame that may
+ * or may not be a sync frame but is closest to or the same as msec.
+ * {@link #SEEK_CLOSEST} often has larger performance overhead compared
+ * to the other options if there is no sync frame located at msec.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized
+ * @throws IllegalArgumentException if the mode is invalid.
+ */
+ public abstract void seekTo(long msec, @SeekMode int mode);
+
+ /**
+ * Get current playback position as a {@link MediaTimestamp}.
+ * <p>
+ * The MediaTimestamp represents how the media time correlates to the system time in
+ * a linear fashion using an anchor and a clock rate. During regular playback, the media
+ * time moves fairly constantly (though the anchor frame may be rebased to a current
+ * system time, the linear correlation stays steady). Therefore, this method does not
+ * need to be called often.
+ * <p>
+ * To help users get current playback position, this method always anchors the timestamp
+ * to the current {@link System#nanoTime system time}, so
+ * {@link MediaTimestamp#getAnchorMediaTimeUs} can be used as current playback position.
+ *
+ * @return a MediaTimestamp object if a timestamp is available, or {@code null} if no timestamp
+ * is available, e.g. because the media player has not been initialized.
+ *
+ * @see MediaTimestamp
+ */
+ @Nullable
+ public abstract MediaTimestamp getTimestamp();
+
+ /**
+ * Gets the current playback position.
+ *
+ * @return the current position in milliseconds
+ */
+ public abstract int getCurrentPosition();
+
+ /**
+ * Gets the duration of the file.
+ *
+ * @return the duration in milliseconds, if no duration is available
+ * (for example, if streaming live content), -1 is returned.
+ */
+ public abstract int getDuration();
+
+ /**
+ * Gets the media metadata.
+ *
+ * @param update_only controls whether the full set of available
+ * metadata is returned or just the set that changed since the
+ * last call. See {@see #METADATA_UPDATE_ONLY} and {@see
+ * #METADATA_ALL}.
+ *
+ * @param apply_filter if true only metadata that matches the
+ * filter is returned. See {@see #APPLY_METADATA_FILTER} and {@see
+ * #BYPASS_METADATA_FILTER}.
+ *
+ * @return The metadata, possibly empty. null if an error occured.
+ // FIXME: unhide.
+ * {@hide}
+ */
+ public Metadata getMetadata(final boolean update_only,
+ final boolean apply_filter) {
+ return null;
+ }
+
+ /**
+ * Set a filter for the metadata update notification and update
+ * retrieval. The caller provides 2 set of metadata keys, allowed
+ * and blocked. The blocked set always takes precedence over the
+ * allowed one.
+ * Metadata.MATCH_ALL and Metadata.MATCH_NONE are 2 sets available as
+ * shorthands to allow/block all or no metadata.
+ *
+ * By default, there is no filter set.
+ *
+ * @param allow Is the set of metadata the client is interested
+ * in receiving new notifications for.
+ * @param block Is the set of metadata the client is not interested
+ * in receiving new notifications for.
+ * @return The call status code.
+ *
+ // FIXME: unhide.
+ * {@hide}
+ */
+ public int setMetadataFilter(Set<Integer> allow, Set<Integer> block) {
+ return 0;
+ }
+
+ /**
+ * Set the MediaPlayer2 to start when this MediaPlayer2 finishes playback
+ * (i.e. reaches the end of the stream).
+ * The media framework will attempt to transition from this player to
+ * the next as seamlessly as possible. The next player can be set at
+ * any time before completion, but shall be after setDataSource has been
+ * called successfully. The next player must be prepared by the
+ * app, and the application should not call play() on it.
+ * The next MediaPlayer2 must be different from 'this'. An exception
+ * will be thrown if next == this.
+ * The application may call setNextMediaPlayer(null) to indicate no
+ * next player should be started at the end of playback.
+ * If the current player is looping, it will keep looping and the next
+ * player will not be started.
+ *
+ * @param next the player to start after this one completes playback.
+ *
+ * @hide
+ */
+ public void setNextMediaPlayer(MediaPlayer2 next) { }
+
+ /**
+ * Resets the MediaPlayer2 to its uninitialized state. After calling
+ * this method, you will have to initialize it again by setting the
+ * data source and calling prepareAsync().
+ */
+ public abstract void reset();
+
+ /**
+ * Set up a timer for {@link #TimeProvider}. {@link #TimeProvider} will be
+ * notified when the presentation time reaches (becomes greater than or equal to)
+ * the value specified.
+ *
+ * @param mediaTimeUs presentation time to get timed event callback at
+ * @hide
+ */
+ public void notifyAt(long mediaTimeUs) { }
+
+ /**
+ * Sets the audio attributes for this MediaPlayer2.
+ * See {@link AudioAttributes} for how to build and configure an instance of this class.
+ * You must call this method before {@link #prepareAsync()} in order
+ * for the audio attributes to become effective thereafter.
+ * @param attributes a non-null set of audio attributes
+ * @throws IllegalArgumentException if the attributes are null or invalid.
+ */
+ public abstract void setAudioAttributes(AudioAttributes attributes);
+
+ /**
+ * Sets the player to be looping or non-looping.
+ *
+ * @param looping whether to loop or not
+ * @hide
+ */
+ public void setLooping(boolean looping) { }
+
+ /**
+ * Checks whether the MediaPlayer2 is looping or non-looping.
+ *
+ * @return true if the MediaPlayer2 is currently looping, false otherwise
+ * @hide
+ */
+ public boolean isLooping() {
+ return false;
+ }
+
+ /**
+ * Sets the volume on this player.
+ * This API is recommended for balancing the output of audio streams
+ * within an application. Unless you are writing an application to
+ * control user settings, this API should be used in preference to
+ * {@link AudioManager#setStreamVolume(int, int, int)} which sets the volume of ALL streams of
+ * a particular type. Note that the passed volume values are raw scalars in range 0.0 to 1.0.
+ * UI controls should be scaled logarithmically.
+ *
+ * @param leftVolume left volume scalar
+ * @param rightVolume right volume scalar
+ */
+ /*
+ * FIXME: Merge this into javadoc comment above when setVolume(float) is not @hide.
+ * The single parameter form below is preferred if the channel volumes don't need
+ * to be set independently.
+ */
+ public abstract void setVolume(float leftVolume, float rightVolume);
+
+ /**
+ * Similar, excepts sets volume of all channels to same value.
+ * @hide
+ */
+ public void setVolume(float volume) { }
+
+ /**
+ * Sets the audio session ID.
+ *
+ * @param sessionId the audio session ID.
+ * The audio session ID is a system wide unique identifier for the audio stream played by
+ * this MediaPlayer2 instance.
+ * The primary use of the audio session ID is to associate audio effects to a particular
+ * instance of MediaPlayer2: if an audio session ID is provided when creating an audio effect,
+ * this effect will be applied only to the audio content of media players within the same
+ * audio session and not to the output mix.
+ * When created, a MediaPlayer2 instance automatically generates its own audio session ID.
+ * However, it is possible to force this player to be part of an already existing audio session
+ * by calling this method.
+ * This method must be called before one of the overloaded <code> setDataSource </code> methods.
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if the sessionId is invalid.
+ */
+ public abstract void setAudioSessionId(int sessionId);
+
+ /**
+ * Returns the audio session ID.
+ *
+ * @return the audio session ID. {@see #setAudioSessionId(int)}
+ * Note that the audio session ID is 0 only if a problem occured when the MediaPlayer2 was contructed.
+ */
+ public abstract int getAudioSessionId();
+
+ /**
+ * Attaches an auxiliary effect to the player. A typical auxiliary effect is a reverberation
+ * effect which can be applied on any sound source that directs a certain amount of its
+ * energy to this effect. This amount is defined by setAuxEffectSendLevel().
+ * See {@link #setAuxEffectSendLevel(float)}.
+ * <p>After creating an auxiliary effect (e.g.
+ * {@link android.media.audiofx.EnvironmentalReverb}), retrieve its ID with
+ * {@link android.media.audiofx.AudioEffect#getId()} and use it when calling this method
+ * to attach the player to the effect.
+ * <p>To detach the effect from the player, call this method with a null effect id.
+ * <p>This method must be called after one of the overloaded <code> setDataSource </code>
+ * methods.
+ * @param effectId system wide unique id of the effect to attach
+ */
+ public abstract void attachAuxEffect(int effectId);
+
+
+ /**
+ * Sets the send level of the player to the attached auxiliary effect.
+ * See {@link #attachAuxEffect(int)}. The level value range is 0 to 1.0.
+ * <p>By default the send level is 0, so even if an effect is attached to the player
+ * this method must be called for the effect to be applied.
+ * <p>Note that the passed level value is a raw scalar. UI controls should be scaled
+ * logarithmically: the gain applied by audio framework ranges from -72dB to 0dB,
+ * so an appropriate conversion from linear UI input x to level is:
+ * x == 0 -> level = 0
+ * 0 < x <= R -> level = 10^(72*(x-R)/20/R)
+ * @param level send level scalar
+ */
+ public abstract void setAuxEffectSendLevel(float level);
+
+ /**
+ * Class for MediaPlayer2 to return each audio/video/subtitle track's metadata.
+ *
+ * @see android.media.MediaPlayer2#getTrackInfo
+ */
+ public abstract static class TrackInfo {
+ /**
+ * Gets the track type.
+ * @return TrackType which indicates if the track is video, audio, timed text.
+ */
+ public abstract int getTrackType();
+
+ /**
+ * Gets the language code of the track.
+ * @return a language code in either way of ISO-639-1 or ISO-639-2.
+ * When the language is unknown or could not be determined,
+ * ISO-639-2 language code, "und", is returned.
+ */
+ public abstract String getLanguage();
+
+ /**
+ * Gets the {@link MediaFormat} of the track. If the format is
+ * unknown or could not be determined, null is returned.
+ */
+ public abstract MediaFormat getFormat();
+
+ public static final int MEDIA_TRACK_TYPE_UNKNOWN = 0;
+ public static final int MEDIA_TRACK_TYPE_VIDEO = 1;
+ public static final int MEDIA_TRACK_TYPE_AUDIO = 2;
+
+ /** @hide */
+ public static final int MEDIA_TRACK_TYPE_TIMEDTEXT = 3;
+
+ public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4;
+ public static final int MEDIA_TRACK_TYPE_METADATA = 5;
+
+ @Override
+ public abstract String toString();
+ };
+
+ /**
+ * Returns a List of track information.
+ *
+ * @return List of track info. The total number of tracks is the array length.
+ * Must be called again if an external timed text source has been added after
+ * addTimedTextSource method is called.
+ * @throws IllegalStateException if it is called in an invalid state.
+ */
+ public abstract List<TrackInfo> getTrackInfo();
+
+ /* Do not change these values without updating their counterparts
+ * in include/media/stagefright/MediaDefs.h and media/libstagefright/MediaDefs.cpp!
+ */
+ /**
+ * MIME type for SubRip (SRT) container. Used in addTimedTextSource APIs.
+ * @hide
+ */
+ public static final String MEDIA_MIMETYPE_TEXT_SUBRIP = "application/x-subrip";
+
+ /**
+ * MIME type for WebVTT subtitle data.
+ * @hide
+ */
+ public static final String MEDIA_MIMETYPE_TEXT_VTT = "text/vtt";
+
+ /**
+ * MIME type for CEA-608 closed caption data.
+ * @hide
+ */
+ public static final String MEDIA_MIMETYPE_TEXT_CEA_608 = "text/cea-608";
+
+ /**
+ * MIME type for CEA-708 closed caption data.
+ * @hide
+ */
+ public static final String MEDIA_MIMETYPE_TEXT_CEA_708 = "text/cea-708";
+
+ /** @hide */
+ public void setSubtitleAnchor(
+ SubtitleController controller,
+ SubtitleController.Anchor anchor) { }
+
+ /** @hide */
+ @Override
+ public void onSubtitleTrackSelected(SubtitleTrack track) { }
+
+ /** @hide */
+ public void addSubtitleSource(InputStream is, MediaFormat format) { }
+
+ /* TODO: Limit the total number of external timed text source to a reasonable number.
+ */
+ /**
+ * Adds an external timed text source file.
+ *
+ * Currently supported format is SubRip with the file extension .srt, case insensitive.
+ * Note that a single external timed text source may contain multiple tracks in it.
+ * One can find the total number of available tracks using {@link #getTrackInfo()} to see what
+ * additional tracks become available after this method call.
+ *
+ * @param path The file path of external timed text source file.
+ * @param mimeType The mime type of the file. Must be one of the mime types listed above.
+ * @throws IOException if the file cannot be accessed or is corrupted.
+ * @throws IllegalArgumentException if the mimeType is not supported.
+ * @throws IllegalStateException if called in an invalid state.
+ * @hide
+ */
+ public void addTimedTextSource(String path, String mimeType) throws IOException { }
+
+ /**
+ * Adds an external timed text source file (Uri).
+ *
+ * Currently supported format is SubRip with the file extension .srt, case insensitive.
+ * Note that a single external timed text source may contain multiple tracks in it.
+ * One can find the total number of available tracks using {@link #getTrackInfo()} to see what
+ * additional tracks become available after this method call.
+ *
+ * @param context the Context to use when resolving the Uri
+ * @param uri the Content URI of the data you want to play
+ * @param mimeType The mime type of the file. Must be one of the mime types listed above.
+ * @throws IOException if the file cannot be accessed or is corrupted.
+ * @throws IllegalArgumentException if the mimeType is not supported.
+ * @throws IllegalStateException if called in an invalid state.
+ * @hide
+ */
+ public void addTimedTextSource(Context context, Uri uri, String mimeType) throws IOException { }
+
+ /**
+ * Adds an external timed text source file (FileDescriptor).
+ *
+ * It is the caller's responsibility to close the file descriptor.
+ * It is safe to do so as soon as this call returns.
+ *
+ * Currently supported format is SubRip. Note that a single external timed text source may
+ * contain multiple tracks in it. One can find the total number of available tracks
+ * using {@link #getTrackInfo()} to see what additional tracks become available
+ * after this method call.
+ *
+ * @param fd the FileDescriptor for the file you want to play
+ * @param mimeType The mime type of the file. Must be one of the mime types listed above.
+ * @throws IllegalArgumentException if the mimeType is not supported.
+ * @throws IllegalStateException if called in an invalid state.
+ * @hide
+ */
+ public void addTimedTextSource(FileDescriptor fd, String mimeType) { }
+
+ /**
+ * Adds an external timed text file (FileDescriptor).
+ *
+ * It is the caller's responsibility to close the file descriptor.
+ * It is safe to do so as soon as this call returns.
+ *
+ * Currently supported format is SubRip. Note that a single external timed text source may
+ * contain multiple tracks in it. One can find the total number of available tracks
+ * using {@link #getTrackInfo()} to see what additional tracks become available
+ * after this method call.
+ *
+ * @param fd the FileDescriptor for the file you want to play
+ * @param offset the offset into the file where the data to be played starts, in bytes
+ * @param length the length in bytes of the data to be played
+ * @param mime The mime type of the file. Must be one of the mime types listed above.
+ * @throws IllegalArgumentException if the mimeType is not supported.
+ * @throws IllegalStateException if called in an invalid state.
+ * @hide
+ */
+ public abstract void addTimedTextSource(FileDescriptor fd, long offset, long length, String mime);
+
+ /**
+ * Returns the index of the audio, video, or subtitle track currently selected for playback,
+ * The return value is an index into the array returned by {@link #getTrackInfo()}, and can
+ * be used in calls to {@link #selectTrack(int)} or {@link #deselectTrack(int)}.
+ *
+ * @param trackType should be one of {@link TrackInfo#MEDIA_TRACK_TYPE_VIDEO},
+ * {@link TrackInfo#MEDIA_TRACK_TYPE_AUDIO}, or
+ * {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE}
+ * @return index of the audio, video, or subtitle track currently selected for playback;
+ * a negative integer is returned when there is no selected track for {@code trackType} or
+ * when {@code trackType} is not one of audio, video, or subtitle.
+ * @throws IllegalStateException if called after {@link #close()}
+ *
+ * @see #getTrackInfo()
+ * @see #selectTrack(int)
+ * @see #deselectTrack(int)
+ */
+ public abstract int getSelectedTrack(int trackType);
+
+ /**
+ * Selects a track.
+ * <p>
+ * If a MediaPlayer2 is in invalid state, it throws an IllegalStateException exception.
+ * If a MediaPlayer2 is in <em>Started</em> state, the selected track is presented immediately.
+ * If a MediaPlayer2 is not in Started state, it just marks the track to be played.
+ * </p>
+ * <p>
+ * In any valid state, if it is called multiple times on the same type of track (ie. Video,
+ * Audio, Timed Text), the most recent one will be chosen.
+ * </p>
+ * <p>
+ * The first audio and video tracks are selected by default if available, even though
+ * this method is not called. However, no timed text track will be selected until
+ * this function is called.
+ * </p>
+ * <p>
+ * Currently, only timed text tracks or audio tracks can be selected via this method.
+ * In addition, the support for selecting an audio track at runtime is pretty limited
+ * in that an audio track can only be selected in the <em>Prepared</em> state.
+ * </p>
+ * @param index the index of the track to be selected. The valid range of the index
+ * is 0..total number of track - 1. The total number of tracks as well as the type of
+ * each individual track can be found by calling {@link #getTrackInfo()} method.
+ * @throws IllegalStateException if called in an invalid state.
+ *
+ * @see android.media.MediaPlayer2#getTrackInfo
+ */
+ public abstract void selectTrack(int index);
+
+ /**
+ * Deselect a track.
+ * <p>
+ * Currently, the track must be a timed text track and no audio or video tracks can be
+ * deselected. If the timed text track identified by index has not been
+ * selected before, it throws an exception.
+ * </p>
+ * @param index the index of the track to be deselected. The valid range of the index
+ * is 0..total number of tracks - 1. The total number of tracks as well as the type of
+ * each individual track can be found by calling {@link #getTrackInfo()} method.
+ * @throws IllegalStateException if called in an invalid state.
+ *
+ * @see android.media.MediaPlayer2#getTrackInfo
+ */
+ public abstract void deselectTrack(int index);
+
+ /**
+ * Sets the target UDP re-transmit endpoint for the low level player.
+ * Generally, the address portion of the endpoint is an IP multicast
+ * address, although a unicast address would be equally valid. When a valid
+ * retransmit endpoint has been set, the media player will not decode and
+ * render the media presentation locally. Instead, the player will attempt
+ * to re-multiplex its media data using the Android@Home RTP profile and
+ * re-transmit to the target endpoint. Receiver devices (which may be
+ * either the same as the transmitting device or different devices) may
+ * instantiate, prepare, and start a receiver player using a setDataSource
+ * URL of the form...
+ *
+ * aahRX://<multicastIP>:<port>
+ *
+ * to receive, decode and render the re-transmitted content.
+ *
+ * setRetransmitEndpoint may only be called before setDataSource has been
+ * called; while the player is in the Idle state.
+ *
+ * @param endpoint the address and UDP port of the re-transmission target or
+ * null if no re-transmission is to be performed.
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if the retransmit endpoint is supplied,
+ * but invalid.
+ *
+ * {@hide} pending API council
+ */
+ public void setRetransmitEndpoint(InetSocketAddress endpoint) { }
+
+ /**
+ * Releases the resources held by this {@code MediaPlayer2} object.
+ *
+ * It is considered good practice to call this method when you're
+ * done using the MediaPlayer2. In particular, whenever an Activity
+ * of an application is paused (its onPause() method is called),
+ * or stopped (its onStop() method is called), this method should be
+ * invoked to release the MediaPlayer2 object, unless the application
+ * has a special need to keep the object around. In addition to
+ * unnecessary resources (such as memory and instances of codecs)
+ * being held, failure to call this method immediately if a
+ * MediaPlayer2 object is no longer needed may also lead to
+ * continuous battery consumption for mobile devices, and playback
+ * failure for other applications if no multiple instances of the
+ * same codec are supported on a device. Even if multiple instances
+ * of the same codec are supported, some performance degradation
+ * may be expected when unnecessary multiple instances are used
+ * at the same time.
+ *
+ * {@code close()} may be safely called after a prior {@code close()}.
+ * This class implements the Java {@code AutoCloseable} interface and
+ * may be used with try-with-resources.
+ */
+ @Override
+ public abstract void close();
+
+ /** @hide */
+ public MediaTimeProvider getMediaTimeProvider() {
+ return null;
+ }
+
+ /**
+ * Interface definition for callbacks to be invoked when the player has the corresponding
+ * events.
+ */
+ public abstract static class EventCallback {
+ /**
+ * Called to update status in buffering a media source received through
+ * progressive downloading. The received buffering percentage
+ * indicates how much of the content has been buffered or played.
+ * For example a buffering update of 80 percent when half the content
+ * has already been played indicates that the next 30 percent of the
+ * content to play has been buffered.
+ *
+ * @param mp the MediaPlayer2 the update pertains to
+ * @param srcId the Id of this data source
+ * @param percent the percentage (0-100) of the content
+ * that has been buffered or played thus far
+ */
+ public void onBufferingUpdate(MediaPlayer2 mp, long srcId, int percent) { }
+
+ /**
+ * Called to indicate the video size
+ *
+ * The video size (width and height) could be 0 if there was no video,
+ * no display surface was set, or the value was not determined yet.
+ *
+ * @param mp the MediaPlayer2 associated with this callback
+ * @param srcId the Id of this data source
+ * @param width the width of the video
+ * @param height the height of the video
+ */
+ public void onVideoSizeChanged(MediaPlayer2 mp, long srcId, int width, int height) { }
+
+ /**
+ * Called to indicate an avaliable timed text
+ *
+ * @param mp the MediaPlayer2 associated with this callback
+ * @param srcId the Id of this data source
+ * @param text the timed text sample which contains the text
+ * needed to be displayed and the display format.
+ * @hide
+ */
+ public void onTimedText(MediaPlayer2 mp, long srcId, TimedText text) { }
+
+ /**
+ * Called to indicate avaliable timed metadata
+ * <p>
+ * This method will be called as timed metadata is extracted from the media,
+ * in the same order as it occurs in the media. The timing of this event is
+ * not controlled by the associated timestamp.
+ * <p>
+ * Currently only HTTP live streaming data URI's embedded with timed ID3 tags generates
+ * {@link TimedMetaData}.
+ *
+ * @see MediaPlayer2#selectTrack(int)
+ * @see MediaPlayer2.OnTimedMetaDataAvailableListener
+ * @see TimedMetaData
+ *
+ * @param mp the MediaPlayer2 associated with this callback
+ * @param srcId the Id of this data source
+ * @param data the timed metadata sample associated with this event
+ */
+ public void onTimedMetaDataAvailable(MediaPlayer2 mp, long srcId, TimedMetaData data) { }
+
+ /**
+ * Called to indicate an error.
+ *
+ * @param mp the MediaPlayer2 the error pertains to
+ * @param srcId the Id of this data source
+ * @param what the type of error that has occurred:
+ * <ul>
+ * <li>{@link #MEDIA_ERROR_UNKNOWN}
+ * </ul>
+ * @param extra an extra code, specific to the error. Typically
+ * implementation dependent.
+ * <ul>
+ * <li>{@link #MEDIA_ERROR_IO}
+ * <li>{@link #MEDIA_ERROR_MALFORMED}
+ * <li>{@link #MEDIA_ERROR_UNSUPPORTED}
+ * <li>{@link #MEDIA_ERROR_TIMED_OUT}
+ * <li><code>MEDIA_ERROR_SYSTEM (-2147483648)</code> - low-level system error.
+ * </ul>
+ */
+ public void onError(MediaPlayer2 mp, long srcId, int what, int extra) { }
+
+ /**
+ * Called to indicate an info or a warning.
+ *
+ * @param mp the MediaPlayer2 the info pertains to.
+ * @param srcId the Id of this data source
+ * @param what the type of info or warning.
+ * <ul>
+ * <li>{@link #MEDIA_INFO_UNKNOWN}
+ * <li>{@link #MEDIA_INFO_STARTED_AS_NEXT}
+ * <li>{@link #MEDIA_INFO_VIDEO_RENDERING_START}
+ * <li>{@link #MEDIA_INFO_AUDIO_RENDERING_START}
+ * <li>{@link #MEDIA_INFO_PLAYBACK_COMPLETE}
+ * <li>{@link #MEDIA_INFO_PLAYLIST_END}
+ * <li>{@link #MEDIA_INFO_PREPARED}
+ * <li>{@link #MEDIA_INFO_COMPLETE_CALL_PLAY}
+ * <li>{@link #MEDIA_INFO_COMPLETE_CALL_PAUSE}
+ * <li>{@link #MEDIA_INFO_COMPLETE_CALL_SEEK}
+ * <li>{@link #MEDIA_INFO_VIDEO_TRACK_LAGGING}
+ * <li>{@link #MEDIA_INFO_BUFFERING_START}
+ * <li>{@link #MEDIA_INFO_BUFFERING_END}
+ * <li><code>MEDIA_INFO_NETWORK_BANDWIDTH (703)</code> -
+ * bandwidth information is available (as <code>extra</code> kbps)
+ * <li>{@link #MEDIA_INFO_BAD_INTERLEAVING}
+ * <li>{@link #MEDIA_INFO_NOT_SEEKABLE}
+ * <li>{@link #MEDIA_INFO_METADATA_UPDATE}
+ * <li>{@link #MEDIA_INFO_UNSUPPORTED_SUBTITLE}
+ * <li>{@link #MEDIA_INFO_SUBTITLE_TIMED_OUT}
+ * </ul>
+ * @param extra an extra code, specific to the info. Typically
+ * implementation dependent.
+ */
+ public void onInfo(MediaPlayer2 mp, long srcId, int what, int extra) { }
+ }
+
+ /**
+ * Register a callback to be invoked when the media source is ready
+ * for playback.
+ *
+ * @param eventCallback the callback that will be run
+ * @param executor the executor through which the callback should be invoked
+ */
+ public abstract void registerEventCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull EventCallback eventCallback);
+
+ /**
+ * Unregisters an {@link EventCallback}.
+ *
+ * @param callback an {@link EventCallback} to unregister
+ */
+ public abstract void unregisterEventCallback(EventCallback callback);
+
+ /**
+ * Interface definition of a callback to be invoked when a
+ * track has data available.
+ *
+ * @hide
+ */
+ public interface OnSubtitleDataListener
+ {
+ public void onSubtitleData(MediaPlayer2 mp, SubtitleData data);
+ }
+
+ /**
+ * Register a callback to be invoked when a track has data available.
+ *
+ * @param listener the callback that will be run
+ *
+ * @hide
+ */
+ public void setOnSubtitleDataListener(OnSubtitleDataListener listener) { }
+
+
+ /* Do not change these values without updating their counterparts
+ * in include/media/mediaplayer2.h!
+ */
+ /** Unspecified media player error.
+ * @see android.media.MediaPlayer2.EventCallback.onError
+ */
+ public static final int MEDIA_ERROR_UNKNOWN = 1;
+
+ /** The video is streamed and its container is not valid for progressive
+ * playback i.e the video's index (e.g moov atom) is not at the start of the
+ * file.
+ * @see android.media.MediaPlayer2.EventCallback.onError
+ */
+ public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200;
+
+ /** File or network related operation errors. */
+ public static final int MEDIA_ERROR_IO = -1004;
+ /** Bitstream is not conforming to the related coding standard or file spec. */
+ public static final int MEDIA_ERROR_MALFORMED = -1007;
+ /** Bitstream is conforming to the related coding standard or file spec, but
+ * the media framework does not support the feature. */
+ public static final int MEDIA_ERROR_UNSUPPORTED = -1010;
+ /** Some operation takes too long to complete, usually more than 3-5 seconds. */
+ public static final int MEDIA_ERROR_TIMED_OUT = -110;
+
+ /** Unspecified low-level system error. This value originated from UNKNOWN_ERROR in
+ * system/core/include/utils/Errors.h
+ * @see android.media.MediaPlayer2.EventCallback.onError
+ * @hide
+ */
+ public static final int MEDIA_ERROR_SYSTEM = -2147483648;
+
+
+ /* Do not change these values without updating their counterparts
+ * in include/media/mediaplayer2.h!
+ */
+ /** Unspecified media player info.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_UNKNOWN = 1;
+
+ /** The player switched to this datas source because it is the
+ * next-to-be-played in the play list.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_STARTED_AS_NEXT = 2;
+
+ /** The player just pushed the very first video frame for rendering.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3;
+
+ /** The player just rendered the very first audio sample.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_AUDIO_RENDERING_START = 4;
+
+ /** The player just completed the playback of this data source.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_PLAYBACK_COMPLETE = 5;
+
+ /** The player just completed the playback of the full play list.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_PLAYLIST_END = 6;
+
+ /** The player just prepared a data source.
+ * This also serves as call completion notification for {@link #prepareAsync()}.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_PREPARED = 100;
+
+ /** The player just completed a call {@link #play()}.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_COMPLETE_CALL_PLAY = 101;
+
+ /** The player just completed a call {@link #pause()}.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_COMPLETE_CALL_PAUSE = 102;
+
+ /** The player just completed a call {@link #seekTo(long, int)}.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_COMPLETE_CALL_SEEK = 103;
+
+ /** The video is too complex for the decoder: it can't decode frames fast
+ * enough. Possibly only the audio plays fine at this stage.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700;
+
+ /** MediaPlayer2 is temporarily pausing playback internally in order to
+ * buffer more data.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_BUFFERING_START = 701;
+
+ /** MediaPlayer2 is resuming playback after filling buffers.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_BUFFERING_END = 702;
+
+ /** Estimated network bandwidth information (kbps) is available; currently this event fires
+ * simultaneously as {@link #MEDIA_INFO_BUFFERING_START} and {@link #MEDIA_INFO_BUFFERING_END}
+ * when playing network files.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ * @hide
+ */
+ public static final int MEDIA_INFO_NETWORK_BANDWIDTH = 703;
+
+ /** Bad interleaving means that a media has been improperly interleaved or
+ * not interleaved at all, e.g has all the video samples first then all the
+ * audio ones. Video is playing but a lot of disk seeks may be happening.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_BAD_INTERLEAVING = 800;
+
+ /** The media cannot be seeked (e.g live stream)
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_NOT_SEEKABLE = 801;
+
+ /** A new set of metadata is available.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_METADATA_UPDATE = 802;
+
+ /** A new set of external-only metadata is available. Used by
+ * JAVA framework to avoid triggering track scanning.
+ * @hide
+ */
+ public static final int MEDIA_INFO_EXTERNAL_METADATA_UPDATE = 803;
+
+ /** Informs that audio is not playing. Note that playback of the video
+ * is not interrupted.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804;
+
+ /** Informs that video is not playing. Note that playback of the audio
+ * is not interrupted.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805;
+
+ /** Failed to handle timed text track properly.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ *
+ * {@hide}
+ */
+ public static final int MEDIA_INFO_TIMED_TEXT_ERROR = 900;
+
+ /** Subtitle track was not supported by the media framework.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_UNSUPPORTED_SUBTITLE = 901;
+
+ /** Reading the subtitle track takes too long.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_SUBTITLE_TIMED_OUT = 902;
+
+
+ // Modular DRM begin
+
+ /**
+ * Interface definition of a callback to be invoked when the app
+ * can do DRM configuration (get/set properties) before the session
+ * is opened. This facilitates configuration of the properties, like
+ * 'securityLevel', which has to be set after DRM scheme creation but
+ * before the DRM session is opened.
+ *
+ * The only allowed DRM calls in this listener are {@code getDrmPropertyString}
+ * and {@code setDrmPropertyString}.
+ */
+ public interface OnDrmConfigHelper
+ {
+ /**
+ * Called to give the app the opportunity to configure DRM before the session is created
+ *
+ * @param mp the {@code MediaPlayer2} associated with this callback
+ */
+ public void onDrmConfig(MediaPlayer2 mp);
+ }
+
+ /**
+ * Register a callback to be invoked for configuration of the DRM object before
+ * the session is created.
+ * The callback will be invoked synchronously during the execution
+ * of {@link #prepareDrm(UUID uuid)}.
+ *
+ * @param listener the callback that will be run
+ */
+ public abstract void setOnDrmConfigHelper(OnDrmConfigHelper listener);
+
+ /**
+ * Interface definition for callbacks to be invoked when the player has the corresponding
+ * DRM events.
+ */
+ public abstract static class DrmEventCallback {
+ /**
+ * Called to indicate DRM info is available
+ *
+ * @param mp the {@code MediaPlayer2} associated with this callback
+ * @param drmInfo DRM info of the source including PSSH, and subset
+ * of crypto schemes supported by this device
+ */
+ public void onDrmInfo(MediaPlayer2 mp, DrmInfo drmInfo) { }
+
+ /**
+ * Called to notify the client that {@code prepareDrm} is finished and ready for key request/response.
+ *
+ * @param mp the {@code MediaPlayer2} associated with this callback
+ * @param status the result of DRM preparation which can be
+ * {@link #PREPARE_DRM_STATUS_SUCCESS},
+ * {@link #PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR},
+ * {@link #PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR}, or
+ * {@link #PREPARE_DRM_STATUS_PREPARATION_ERROR}.
+ */
+ public void onDrmPrepared(MediaPlayer2 mp, @PrepareDrmStatusCode int status) { }
+
+ }
+
+ /**
+ * Register a callback to be invoked when the media source is ready
+ * for playback.
+ *
+ * @param eventCallback the callback that will be run
+ * @param executor the executor through which the callback should be invoked
+ */
+ public abstract void registerDrmEventCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull DrmEventCallback eventCallback);
+
+ /**
+ * Unregisters a {@link DrmEventCallback}.
+ *
+ * @param callback a {@link DrmEventCallback} to unregister
+ */
+ public abstract void unregisterDrmEventCallback(DrmEventCallback callback);
+
+ /**
+ * The status codes for {@link DrmEventCallback#onDrmPrepared} listener.
+ * <p>
+ *
+ * DRM preparation has succeeded.
+ */
+ public static final int PREPARE_DRM_STATUS_SUCCESS = 0;
+
+ /**
+ * The device required DRM provisioning but couldn't reach the provisioning server.
+ */
+ public static final int PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR = 1;
+
+ /**
+ * The device required DRM provisioning but the provisioning server denied the request.
+ */
+ public static final int PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR = 2;
+
+ /**
+ * The DRM preparation has failed .
+ */
+ public static final int PREPARE_DRM_STATUS_PREPARATION_ERROR = 3;
+
+
+ /** @hide */
+ @IntDef({
+ PREPARE_DRM_STATUS_SUCCESS,
+ PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR,
+ PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR,
+ PREPARE_DRM_STATUS_PREPARATION_ERROR,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PrepareDrmStatusCode {}
+
+ /**
+ * Retrieves the DRM Info associated with the current source
+ *
+ * @throws IllegalStateException if called before being prepared
+ */
+ public abstract DrmInfo getDrmInfo();
+
+ /**
+ * Prepares the DRM for the current source
+ * <p>
+ * If {@code OnDrmConfigHelper} is registered, it will be called during
+ * preparation to allow configuration of the DRM properties before opening the
+ * DRM session. Note that the callback is called synchronously in the thread that called
+ * {@code prepareDrm}. It should be used only for a series of {@code getDrmPropertyString}
+ * and {@code setDrmPropertyString} calls and refrain from any lengthy operation.
+ * <p>
+ * If the device has not been provisioned before, this call also provisions the device
+ * which involves accessing the provisioning server and can take a variable time to
+ * complete depending on the network connectivity.
+ * If {@code OnDrmPreparedListener} is registered, prepareDrm() runs in non-blocking
+ * mode by launching the provisioning in the background and returning. The listener
+ * will be called when provisioning and preparation has finished. If a
+ * {@code OnDrmPreparedListener} is not registered, prepareDrm() waits till provisioning
+ * and preparation has finished, i.e., runs in blocking mode.
+ * <p>
+ * If {@code OnDrmPreparedListener} is registered, it is called to indicate the DRM
+ * session being ready. The application should not make any assumption about its call
+ * sequence (e.g., before or after prepareDrm returns), or the thread context that will
+ * execute the listener (unless the listener is registered with a handler thread).
+ * <p>
+ *
+ * @param uuid The UUID of the crypto scheme. If not known beforehand, it can be retrieved
+ * from the source through {@code getDrmInfo} or registering a {@code onDrmInfoListener}.
+ *
+ * @throws IllegalStateException if called before being prepared or the DRM was
+ * prepared already
+ * @throws UnsupportedSchemeException if the crypto scheme is not supported
+ * @throws ResourceBusyException if required DRM resources are in use
+ * @throws ProvisioningNetworkErrorException if provisioning is required but failed due to a
+ * network error
+ * @throws ProvisioningServerErrorException if provisioning is required but failed due to
+ * the request denied by the provisioning server
+ */
+ public abstract void prepareDrm(@NonNull UUID uuid)
+ throws UnsupportedSchemeException, ResourceBusyException,
+ ProvisioningNetworkErrorException, ProvisioningServerErrorException;
+
+ /**
+ * Releases the DRM session
+ * <p>
+ * The player has to have an active DRM session and be in stopped, or prepared
+ * state before this call is made.
+ * A {@code reset()} call will release the DRM session implicitly.
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session to release
+ */
+ public abstract void releaseDrm() throws NoDrmSchemeException;
+
+ /**
+ * A key request/response exchange occurs between the app and a license server
+ * to obtain or release keys used to decrypt encrypted content.
+ * <p>
+ * getKeyRequest() is used to obtain an opaque key request byte array that is
+ * delivered to the license server. The opaque key request byte array is returned
+ * in KeyRequest.data. The recommended URL to deliver the key request to is
+ * returned in KeyRequest.defaultUrl.
+ * <p>
+ * After the app has received the key request response from the server,
+ * it should deliver to the response to the DRM engine plugin using the method
+ * {@link #provideKeyResponse}.
+ *
+ * @param keySetId is the key-set identifier of the offline keys being released when keyType is
+ * {@link MediaDrm#KEY_TYPE_RELEASE}. It should be set to null for other key requests, when
+ * keyType is {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}.
+ *
+ * @param initData is the container-specific initialization data when the keyType is
+ * {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}. Its meaning is
+ * interpreted based on the mime type provided in the mimeType parameter. It could
+ * contain, for example, the content ID, key ID or other data obtained from the content
+ * metadata that is required in generating the key request.
+ * When the keyType is {@link MediaDrm#KEY_TYPE_RELEASE}, it should be set to null.
+ *
+ * @param mimeType identifies the mime type of the content
+ *
+ * @param keyType specifies the type of the request. The request may be to acquire
+ * keys for streaming, {@link MediaDrm#KEY_TYPE_STREAMING}, or for offline content
+ * {@link MediaDrm#KEY_TYPE_OFFLINE}, or to release previously acquired
+ * keys ({@link MediaDrm#KEY_TYPE_RELEASE}), which are identified by a keySetId.
+ *
+ * @param optionalParameters are included in the key request message to
+ * allow a client application to provide additional message parameters to the server.
+ * This may be {@code null} if no additional parameters are to be sent.
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session
+ */
+ @NonNull
+ public abstract MediaDrm.KeyRequest getKeyRequest(@Nullable byte[] keySetId, @Nullable byte[] initData,
+ @Nullable String mimeType, @MediaDrm.KeyType int keyType,
+ @Nullable Map<String, String> optionalParameters)
+ throws NoDrmSchemeException;
+
+ /**
+ * A key response is received from the license server by the app, then it is
+ * provided to the DRM engine plugin using provideKeyResponse. When the
+ * response is for an offline key request, a key-set identifier is returned that
+ * can be used to later restore the keys to a new session with the method
+ * {@ link # restoreKeys}.
+ * When the response is for a streaming or release request, null is returned.
+ *
+ * @param keySetId When the response is for a release request, keySetId identifies
+ * the saved key associated with the release request (i.e., the same keySetId
+ * passed to the earlier {@ link # getKeyRequest} call. It MUST be null when the
+ * response is for either streaming or offline key requests.
+ *
+ * @param response the byte array response from the server
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session
+ * @throws DeniedByServerException if the response indicates that the
+ * server rejected the request
+ */
+ public abstract byte[] provideKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response)
+ throws NoDrmSchemeException, DeniedByServerException;
+
+ /**
+ * Restore persisted offline keys into a new session. keySetId identifies the
+ * keys to load, obtained from a prior call to {@link #provideKeyResponse}.
+ *
+ * @param keySetId identifies the saved key set to restore
+ */
+ public abstract void restoreKeys(@NonNull byte[] keySetId)
+ throws NoDrmSchemeException;
+
+ /**
+ * Read a DRM engine plugin String property value, given the property name string.
+ * <p>
+ * @param propertyName the property name
+ *
+ * Standard fields names are:
+ * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+ * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+ */
+ @NonNull
+ public abstract String getDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName)
+ throws NoDrmSchemeException;
+
+ /**
+ * Set a DRM engine plugin String property value.
+ * <p>
+ * @param propertyName the property name
+ * @param value the property value
+ *
+ * Standard fields names are:
+ * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+ * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+ */
+ public abstract void setDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName,
+ @NonNull String value)
+ throws NoDrmSchemeException;
+
+ /**
+ * Encapsulates the DRM properties of the source.
+ */
+ public abstract static class DrmInfo {
+ /**
+ * Returns the PSSH info of the data source for each supported DRM scheme.
+ */
+ public abstract Map<UUID, byte[]> getPssh();
+
+ /**
+ * Returns the intersection of the data source and the device DRM schemes.
+ * It effectively identifies the subset of the source's DRM schemes which
+ * are supported by the device too.
+ */
+ public abstract List<UUID> getSupportedSchemes();
+ }; // DrmInfo
+
+ /**
+ * Thrown when a DRM method is called before preparing a DRM scheme through prepareDrm().
+ * Extends MediaDrm.MediaDrmException
+ */
+ public abstract static class NoDrmSchemeException extends MediaDrmException {
+ protected NoDrmSchemeException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ /**
+ * Thrown when the device requires DRM provisioning but the provisioning attempt has
+ * failed due to a network error (Internet reachability, timeout, etc.).
+ * Extends MediaDrm.MediaDrmException
+ */
+ public abstract static class ProvisioningNetworkErrorException extends MediaDrmException {
+ protected ProvisioningNetworkErrorException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ /**
+ * Thrown when the device requires DRM provisioning but the provisioning attempt has
+ * failed due to the provisioning server denying the request.
+ * Extends MediaDrm.MediaDrmException
+ */
+ public abstract static class ProvisioningServerErrorException extends MediaDrmException {
+ protected ProvisioningServerErrorException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ public static final class MetricsConstants {
+ private MetricsConstants() {}
+
+ /**
+ * Key to extract the MIME type of the video track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a String.
+ */
+ public static final String MIME_TYPE_VIDEO = "android.media.mediaplayer.video.mime";
+
+ /**
+ * Key to extract the codec being used to decode the video track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a String.
+ */
+ public static final String CODEC_VIDEO = "android.media.mediaplayer.video.codec";
+
+ /**
+ * Key to extract the width (in pixels) of the video track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String WIDTH = "android.media.mediaplayer.width";
+
+ /**
+ * Key to extract the height (in pixels) of the video track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String HEIGHT = "android.media.mediaplayer.height";
+
+ /**
+ * Key to extract the count of video frames played
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String FRAMES = "android.media.mediaplayer.frames";
+
+ /**
+ * Key to extract the count of video frames dropped
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String FRAMES_DROPPED = "android.media.mediaplayer.dropped";
+
+ /**
+ * Key to extract the MIME type of the audio track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a String.
+ */
+ public static final String MIME_TYPE_AUDIO = "android.media.mediaplayer.audio.mime";
+
+ /**
+ * Key to extract the codec being used to decode the audio track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a String.
+ */
+ public static final String CODEC_AUDIO = "android.media.mediaplayer.audio.codec";
+
+ /**
+ * Key to extract the duration (in milliseconds) of the
+ * media being played
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a long.
+ */
+ public static final String DURATION = "android.media.mediaplayer.durationMs";
+
+ /**
+ * Key to extract the playing time (in milliseconds) of the
+ * media being played
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a long.
+ */
+ public static final String PLAYING = "android.media.mediaplayer.playingMs";
+
+ /**
+ * Key to extract the count of errors encountered while
+ * playing the media
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String ERRORS = "android.media.mediaplayer.err";
+
+ /**
+ * Key to extract an (optional) error code detected while
+ * playing the media
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String ERROR_CODE = "android.media.mediaplayer.errcode";
+
+ }
+}
diff --git a/android/media/MediaPlayer2Impl.java b/android/media/MediaPlayer2Impl.java
new file mode 100644
index 0000000..86a285c
--- /dev/null
+++ b/android/media/MediaPlayer2Impl.java
@@ -0,0 +1,4899 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.os.PowerManager;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+import android.util.Pair;
+import android.util.ArrayMap;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.widget.VideoView;
+import android.graphics.SurfaceTexture;
+import android.media.AudioManager;
+import android.media.MediaDrm;
+import android.media.MediaFormat;
+import android.media.MediaPlayer2;
+import android.media.MediaTimeProvider;
+import android.media.PlaybackParams;
+import android.media.SubtitleController;
+import android.media.SubtitleController.Anchor;
+import android.media.SubtitleData;
+import android.media.SubtitleTrack.RenderingWidget;
+import android.media.SyncParams;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import dalvik.system.CloseGuard;
+
+import libcore.io.IoBridge;
+import libcore.io.Streams;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.AutoCloseable;
+import java.lang.Runnable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.HttpCookie;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.net.URL;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.concurrent.Executor;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.Set;
+import java.util.UUID;
+import java.util.Vector;
+
+
+/**
+ * MediaPlayer2 class can be used to control playback
+ * of audio/video files and streams. An example on how to use the methods in
+ * this class can be found in {@link android.widget.VideoView}.
+ *
+ * <p>Topics covered here are:
+ * <ol>
+ * <li><a href="#StateDiagram">State Diagram</a>
+ * <li><a href="#Valid_and_Invalid_States">Valid and Invalid States</a>
+ * <li><a href="#Permissions">Permissions</a>
+ * <li><a href="#Callbacks">Register informational and error callbacks</a>
+ * </ol>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about how to use MediaPlayer2, read the
+ * <a href="{@docRoot}guide/topics/media/mediaplayer.html">Media Playback</a> developer guide.</p>
+ * </div>
+ *
+ * <a name="StateDiagram"></a>
+ * <h3>State Diagram</h3>
+ *
+ * <p>Playback control of audio/video files and streams is managed as a state
+ * machine. The following diagram shows the life cycle and the states of a
+ * MediaPlayer2 object driven by the supported playback control operations.
+ * The ovals represent the states a MediaPlayer2 object may reside
+ * in. The arcs represent the playback control operations that drive the object
+ * state transition. There are two types of arcs. The arcs with a single arrow
+ * head represent synchronous method calls, while those with
+ * a double arrow head represent asynchronous method calls.</p>
+ *
+ * <p><img src="../../../images/mediaplayer_state_diagram.gif"
+ * alt="MediaPlayer State diagram"
+ * border="0" /></p>
+ *
+ * <p>From this state diagram, one can see that a MediaPlayer2 object has the
+ * following states:</p>
+ * <ul>
+ * <li>When a MediaPlayer2 object is just created using <code>new</code> or
+ * after {@link #reset()} is called, it is in the <em>Idle</em> state; and after
+ * {@link #close()} is called, it is in the <em>End</em> state. Between these
+ * two states is the life cycle of the MediaPlayer2 object.
+ * <ul>
+ * <li>There is a subtle but important difference between a newly constructed
+ * MediaPlayer2 object and the MediaPlayer2 object after {@link #reset()}
+ * is called. It is a programming error to invoke methods such
+ * as {@link #getCurrentPosition()},
+ * {@link #getDuration()}, {@link #getVideoHeight()},
+ * {@link #getVideoWidth()}, {@link #setAudioAttributes(AudioAttributes)},
+ * {@link #setLooping(boolean)},
+ * {@link #setVolume(float, float)}, {@link #pause()}, {@link #play()},
+ * {@link #seekTo(long, int)}, {@link #prepare()} or
+ * {@link #prepareAsync()} in the <em>Idle</em> state for both cases. If any of these
+ * methods is called right after a MediaPlayer2 object is constructed,
+ * the user supplied callback method OnErrorListener.onError() won't be
+ * called by the internal player engine and the object state remains
+ * unchanged; but if these methods are called right after {@link #reset()},
+ * the user supplied callback method OnErrorListener.onError() will be
+ * invoked by the internal player engine and the object will be
+ * transfered to the <em>Error</em> state. </li>
+ * <li>It is also recommended that once
+ * a MediaPlayer2 object is no longer being used, call {@link #close()} immediately
+ * so that resources used by the internal player engine associated with the
+ * MediaPlayer2 object can be released immediately. Resource may include
+ * singleton resources such as hardware acceleration components and
+ * failure to call {@link #close()} may cause subsequent instances of
+ * MediaPlayer2 objects to fallback to software implementations or fail
+ * altogether. Once the MediaPlayer2
+ * object is in the <em>End</em> state, it can no longer be used and
+ * there is no way to bring it back to any other state. </li>
+ * <li>Furthermore,
+ * the MediaPlayer2 objects created using <code>new</code> is in the
+ * <em>Idle</em> state.
+ * </li>
+ * </ul>
+ * </li>
+ * <li>In general, some playback control operation may fail due to various
+ * reasons, such as unsupported audio/video format, poorly interleaved
+ * audio/video, resolution too high, streaming timeout, and the like.
+ * Thus, error reporting and recovery is an important concern under
+ * these circumstances. Sometimes, due to programming errors, invoking a playback
+ * control operation in an invalid state may also occur. Under all these
+ * error conditions, the internal player engine invokes a user supplied
+ * EventCallback.onError() method if an EventCallback has been
+ * registered beforehand via
+ * {@link #registerEventCallback(Executor, EventCallback)}.
+ * <ul>
+ * <li>It is important to note that once an error occurs, the
+ * MediaPlayer2 object enters the <em>Error</em> state (except as noted
+ * above), even if an error listener has not been registered by the application.</li>
+ * <li>In order to reuse a MediaPlayer2 object that is in the <em>
+ * Error</em> state and recover from the error,
+ * {@link #reset()} can be called to restore the object to its <em>Idle</em>
+ * state.</li>
+ * <li>It is good programming practice to have your application
+ * register a OnErrorListener to look out for error notifications from
+ * the internal player engine.</li>
+ * <li>IllegalStateException is
+ * thrown to prevent programming errors such as calling {@link #prepare()},
+ * {@link #prepareAsync()}, {@link #setDataSource(DataSourceDesc)}, or
+ * {@code setPlaylist} methods in an invalid state. </li>
+ * </ul>
+ * </li>
+ * <li>Calling
+ * {@link #setDataSource(DataSourceDesc)}, or
+ * {@code setPlaylist} transfers a
+ * MediaPlayer2 object in the <em>Idle</em> state to the
+ * <em>Initialized</em> state.
+ * <ul>
+ * <li>An IllegalStateException is thrown if
+ * setDataSource() or setPlaylist() is called in any other state.</li>
+ * <li>It is good programming
+ * practice to always look out for <code>IllegalArgumentException</code>
+ * and <code>IOException</code> that may be thrown from
+ * <code>setDataSource</code> and <code>setPlaylist</code> methods.</li>
+ * </ul>
+ * </li>
+ * <li>A MediaPlayer2 object must first enter the <em>Prepared</em> state
+ * before playback can be started.
+ * <ul>
+ * <li>There are two ways (synchronous vs.
+ * asynchronous) that the <em>Prepared</em> state can be reached:
+ * either a call to {@link #prepare()} (synchronous) which
+ * transfers the object to the <em>Prepared</em> state once the method call
+ * returns, or a call to {@link #prepareAsync()} (asynchronous) which
+ * first transfers the object to the <em>Preparing</em> state after the
+ * call returns (which occurs almost right way) while the internal
+ * player engine continues working on the rest of preparation work
+ * until the preparation work completes. When the preparation completes or when {@link #prepare()} call returns,
+ * the internal player engine then calls a user supplied callback method,
+ * onPrepared() of the EventCallback interface, if an
+ * EventCallback is registered beforehand via {@link
+ * #registerEventCallback(Executor, EventCallback)}.</li>
+ * <li>It is important to note that
+ * the <em>Preparing</em> state is a transient state, and the behavior
+ * of calling any method with side effect while a MediaPlayer2 object is
+ * in the <em>Preparing</em> state is undefined.</li>
+ * <li>An IllegalStateException is
+ * thrown if {@link #prepare()} or {@link #prepareAsync()} is called in
+ * any other state.</li>
+ * <li>While in the <em>Prepared</em> state, properties
+ * such as audio/sound volume, screenOnWhilePlaying, looping can be
+ * adjusted by invoking the corresponding set methods.</li>
+ * </ul>
+ * </li>
+ * <li>To start the playback, {@link #play()} must be called. After
+ * {@link #play()} returns successfully, the MediaPlayer2 object is in the
+ * <em>Started</em> state. {@link #isPlaying()} can be called to test
+ * whether the MediaPlayer2 object is in the <em>Started</em> state.
+ * <ul>
+ * <li>While in the <em>Started</em> state, the internal player engine calls
+ * a user supplied EventCallback.onBufferingUpdate() callback
+ * method if an EventCallback has been registered beforehand
+ * via {@link #registerEventCallback(Executor, EventCallback)}.
+ * This callback allows applications to keep track of the buffering status
+ * while streaming audio/video.</li>
+ * <li>Calling {@link #play()} has not effect
+ * on a MediaPlayer2 object that is already in the <em>Started</em> state.</li>
+ * </ul>
+ * </li>
+ * <li>Playback can be paused and stopped, and the current playback position
+ * can be adjusted. Playback can be paused via {@link #pause()}. When the call to
+ * {@link #pause()} returns, the MediaPlayer2 object enters the
+ * <em>Paused</em> state. Note that the transition from the <em>Started</em>
+ * state to the <em>Paused</em> state and vice versa happens
+ * asynchronously in the player engine. It may take some time before
+ * the state is updated in calls to {@link #isPlaying()}, and it can be
+ * a number of seconds in the case of streamed content.
+ * <ul>
+ * <li>Calling {@link #play()} to resume playback for a paused
+ * MediaPlayer2 object, and the resumed playback
+ * position is the same as where it was paused. When the call to
+ * {@link #play()} returns, the paused MediaPlayer2 object goes back to
+ * the <em>Started</em> state.</li>
+ * <li>Calling {@link #pause()} has no effect on
+ * a MediaPlayer2 object that is already in the <em>Paused</em> state.</li>
+ * </ul>
+ * </li>
+ * <li>The playback position can be adjusted with a call to
+ * {@link #seekTo(long, int)}.
+ * <ul>
+ * <li>Although the asynchronuous {@link #seekTo(long, int)}
+ * call returns right away, the actual seek operation may take a while to
+ * finish, especially for audio/video being streamed. When the actual
+ * seek operation completes, the internal player engine calls a user
+ * supplied EventCallback.onSeekComplete() if an EventCallback
+ * has been registered beforehand via
+ * {@link #registerEventCallback(Executor, EventCallback)}.</li>
+ * <li>Please
+ * note that {@link #seekTo(long, int)} can also be called in the other states,
+ * such as <em>Prepared</em>, <em>Paused</em> and <em>PlaybackCompleted
+ * </em> state. When {@link #seekTo(long, int)} is called in those states,
+ * one video frame will be displayed if the stream has video and the requested
+ * position is valid.
+ * </li>
+ * <li>Furthermore, the actual current playback position
+ * can be retrieved with a call to {@link #getCurrentPosition()}, which
+ * is helpful for applications such as a Music player that need to keep
+ * track of the playback progress.</li>
+ * </ul>
+ * </li>
+ * <li>When the playback reaches the end of stream, the playback completes.
+ * <ul>
+ * <li>If the looping mode was being set to <var>true</var>with
+ * {@link #setLooping(boolean)}, the MediaPlayer2 object shall remain in
+ * the <em>Started</em> state.</li>
+ * <li>If the looping mode was set to <var>false
+ * </var>, the player engine calls a user supplied callback method,
+ * EventCallback.onCompletion(), if an EventCallback is registered
+ * beforehand via {@link #registerEventCallback(Executor, EventCallback)}.
+ * The invoke of the callback signals that the object is now in the <em>
+ * PlaybackCompleted</em> state.</li>
+ * <li>While in the <em>PlaybackCompleted</em>
+ * state, calling {@link #play()} can restart the playback from the
+ * beginning of the audio/video source.</li>
+ * </ul>
+ *
+ *
+ * <a name="Valid_and_Invalid_States"></a>
+ * <h3>Valid and invalid states</h3>
+ *
+ * <table border="0" cellspacing="0" cellpadding="0">
+ * <tr><td>Method Name </p></td>
+ * <td>Valid Sates </p></td>
+ * <td>Invalid States </p></td>
+ * <td>Comments </p></td></tr>
+ * <tr><td>attachAuxEffect </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
+ * <td>{Idle, Error} </p></td>
+ * <td>This method must be called after setDataSource or setPlaylist.
+ * Calling it does not change the object state. </p></td></tr>
+ * <tr><td>getAudioSessionId </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>getCurrentPosition </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted} </p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change the
+ * state. Calling this method in an invalid state transfers the object
+ * to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getDuration </p></td>
+ * <td>{Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
+ * <td>{Idle, Initialized, Error} </p></td>
+ * <td>Successful invoke of this method in a valid state does not change the
+ * state. Calling this method in an invalid state transfers the object
+ * to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getVideoHeight </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change the
+ * state. Calling this method in an invalid state transfers the object
+ * to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getVideoWidth </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an invalid state transfers the
+ * object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>isPlaying </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an invalid state transfers the
+ * object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>pause </p></td>
+ * <td>{Started, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Prepared, Stopped, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Paused</em> state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>prepare </p></td>
+ * <td>{Initialized, Stopped} </p></td>
+ * <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Prepared</em> state. Calling this method in an
+ * invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>prepareAsync </p></td>
+ * <td>{Initialized, Stopped} </p></td>
+ * <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Preparing</em> state. Calling this method in an
+ * invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>release </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>After {@link #close()}, the object is no longer available. </p></td></tr>
+ * <tr><td>reset </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted, Error}</p></td>
+ * <td>{}</p></td>
+ * <td>After {@link #reset()}, the object is like being just created.</p></td></tr>
+ * <tr><td>seekTo </p></td>
+ * <td>{Prepared, Started, Paused, PlaybackCompleted} </p></td>
+ * <td>{Idle, Initialized, Stopped, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an invalid state transfers the
+ * object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>setAudioAttributes </p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method does not change the state. In order for the
+ * target audio attributes type to become effective, this method must be called before
+ * prepare() or prepareAsync().</p></td></tr>
+ * <tr><td>setAudioSessionId </p></td>
+ * <td>{Idle} </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
+ * Error} </p></td>
+ * <td>This method must be called in idle state as the audio session ID must be known before
+ * calling setDataSource or setPlaylist. Calling it does not change the object
+ * state. </p></td></tr>
+ * <tr><td>setAudioStreamType (deprecated)</p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method does not change the state. In order for the
+ * target audio stream type to become effective, this method must be called before
+ * prepare() or prepareAsync().</p></td></tr>
+ * <tr><td>setAuxEffectSendLevel </p></td>
+ * <td>any</p></td>
+ * <td>{} </p></td>
+ * <td>Calling this method does not change the object state. </p></td></tr>
+ * <tr><td>setDataSource </p></td>
+ * <td>{Idle} </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
+ * Error} </p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Initialized</em> state. Calling this method in an
+ * invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>setPlaylist </p></td>
+ * <td>{Idle} </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
+ * Error} </p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Initialized</em> state. Calling this method in an
+ * invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>setDisplay </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setSurface </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setVideoScalingMode </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
+ * <td>{Idle, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ * <tr><td>setLooping </p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>isLooping </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>registerDrmEventCallback </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>registerEventCallback </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setPlaybackParams</p></td>
+ * <td>{Initialized, Prepared, Started, Paused, PlaybackCompleted, Error}</p></td>
+ * <td>{Idle, Stopped} </p></td>
+ * <td>This method will change state in some cases, depending on when it's called.
+ * </p></td></tr>
+ * <tr><td>setScreenOnWhilePlaying</></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setVolume </p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.
+ * <tr><td>setWakeMode </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state.</p></td></tr>
+ * <tr><td>start </p></td>
+ * <td>{Prepared, Started, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Stopped, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Started</em> state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>stop </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Stopped</em> state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>getTrackInfo </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ * <tr><td>addTimedTextSource </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ * <tr><td>selectTrack </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ * <tr><td>deselectTrack </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ *
+ * </table>
+ *
+ * <a name="Permissions"></a>
+ * <h3>Permissions</h3>
+ * <p>One may need to declare a corresponding WAKE_LOCK permission {@link
+ * android.R.styleable#AndroidManifestUsesPermission <uses-permission>}
+ * element.
+ *
+ * <p>This class requires the {@link android.Manifest.permission#INTERNET} permission
+ * when used with network-based content.
+ *
+ * <a name="Callbacks"></a>
+ * <h3>Callbacks</h3>
+ * <p>Applications may want to register for informational and error
+ * events in order to be informed of some internal state update and
+ * possible runtime errors during playback or streaming. Registration for
+ * these events is done by properly setting the appropriate listeners (via calls
+ * to
+ * {@link #registerEventCallback(Executor, EventCallback)},
+ * {@link #registerDrmEventCallback(Executor, DrmEventCallback)}).
+ * In order to receive the respective callback
+ * associated with these listeners, applications are required to create
+ * MediaPlayer2 objects on a thread with its own Looper running (main UI
+ * thread by default has a Looper running).
+ *
+ * @hide
+ */
+public final class MediaPlayer2Impl extends MediaPlayer2 {
+ static {
+ System.loadLibrary("media2_jni");
+ native_init();
+ }
+
+ private final static String TAG = "MediaPlayer2Impl";
+
+ private long mNativeContext; // accessed by native methods
+ private long mNativeSurfaceTexture; // accessed by native methods
+ private int mListenerContext; // accessed by native methods
+ private SurfaceHolder mSurfaceHolder;
+ private EventHandler mEventHandler;
+ private PowerManager.WakeLock mWakeLock = null;
+ private boolean mScreenOnWhilePlaying;
+ private boolean mStayAwake;
+ private int mStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE;
+ private int mUsage = -1;
+ private boolean mBypassInterruptionPolicy;
+ private final CloseGuard mGuard = CloseGuard.get();
+
+ private List<DataSourceDesc> mPlaylist;
+ private int mPLCurrentIndex = 0;
+ private int mPLNextIndex = -1;
+ private int mLoopingMode = LOOPING_MODE_NONE;
+
+ // Modular DRM
+ private UUID mDrmUUID;
+ private final Object mDrmLock = new Object();
+ private DrmInfoImpl mDrmInfoImpl;
+ private MediaDrm mDrmObj;
+ private byte[] mDrmSessionId;
+ private boolean mDrmInfoResolved;
+ private boolean mActiveDrmScheme;
+ private boolean mDrmConfigAllowed;
+ private boolean mDrmProvisioningInProgress;
+ private boolean mPrepareDrmInProgress;
+ private ProvisioningThread mDrmProvisioningThread;
+
+ /**
+ * Default constructor.
+ * <p>When done with the MediaPlayer2Impl, you should call {@link #close()},
+ * to free the resources. If not released, too many MediaPlayer2Impl instances may
+ * result in an exception.</p>
+ */
+ public MediaPlayer2Impl() {
+ Looper looper;
+ if ((looper = Looper.myLooper()) != null) {
+ mEventHandler = new EventHandler(this, looper);
+ } else if ((looper = Looper.getMainLooper()) != null) {
+ mEventHandler = new EventHandler(this, looper);
+ } else {
+ mEventHandler = null;
+ }
+
+ mTimeProvider = new TimeProvider(this);
+ mOpenSubtitleSources = new Vector<InputStream>();
+ mGuard.open("close");
+
+ /* Native setup requires a weak reference to our object.
+ * It's easier to create it here than in C++.
+ */
+ native_setup(new WeakReference<MediaPlayer2Impl>(this));
+ }
+
+ /*
+ * Update the MediaPlayer2Impl SurfaceTexture.
+ * Call after setting a new display surface.
+ */
+ private native void _setVideoSurface(Surface surface);
+
+ /* Do not change these values (starting with INVOKE_ID) without updating
+ * their counterparts in include/media/mediaplayer2.h!
+ */
+ private static final int INVOKE_ID_GET_TRACK_INFO = 1;
+ private static final int INVOKE_ID_ADD_EXTERNAL_SOURCE = 2;
+ private static final int INVOKE_ID_ADD_EXTERNAL_SOURCE_FD = 3;
+ private static final int INVOKE_ID_SELECT_TRACK = 4;
+ private static final int INVOKE_ID_DESELECT_TRACK = 5;
+ private static final int INVOKE_ID_SET_VIDEO_SCALE_MODE = 6;
+ private static final int INVOKE_ID_GET_SELECTED_TRACK = 7;
+
+ /**
+ * Create a request parcel which can be routed to the native media
+ * player using {@link #invoke(Parcel, Parcel)}. The Parcel
+ * returned has the proper InterfaceToken set. The caller should
+ * not overwrite that token, i.e it can only append data to the
+ * Parcel.
+ *
+ * @return A parcel suitable to hold a request for the native
+ * player.
+ * {@hide}
+ */
+ @Override
+ public Parcel newRequest() {
+ Parcel parcel = Parcel.obtain();
+ return parcel;
+ }
+
+ /**
+ * Invoke a generic method on the native player using opaque
+ * parcels for the request and reply. Both payloads' format is a
+ * convention between the java caller and the native player.
+ * Must be called after setDataSource or setPlaylist to make sure a native player
+ * exists. On failure, a RuntimeException is thrown.
+ *
+ * @param request Parcel with the data for the extension. The
+ * caller must use {@link #newRequest()} to get one.
+ *
+ * @param reply Output parcel with the data returned by the
+ * native player.
+ * {@hide}
+ */
+ @Override
+ public void invoke(Parcel request, Parcel reply) {
+ int retcode = native_invoke(request, reply);
+ reply.setDataPosition(0);
+ if (retcode != 0) {
+ throw new RuntimeException("failure code: " + retcode);
+ }
+ }
+
+ /**
+ * Sets the {@link SurfaceHolder} to use for displaying the video
+ * portion of the media.
+ *
+ * Either a surface holder or surface must be set if a display or video sink
+ * is needed. Not calling this method or {@link #setSurface(Surface)}
+ * when playing back a video will result in only the audio track being played.
+ * A null surface holder or surface will result in only the audio track being
+ * played.
+ *
+ * @param sh the SurfaceHolder to use for video display
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ * @hide
+ */
+ @Override
+ public void setDisplay(SurfaceHolder sh) {
+ mSurfaceHolder = sh;
+ Surface surface;
+ if (sh != null) {
+ surface = sh.getSurface();
+ } else {
+ surface = null;
+ }
+ _setVideoSurface(surface);
+ updateSurfaceScreenOn();
+ }
+
+ /**
+ * Sets the {@link Surface} to be used as the sink for the video portion of
+ * the media. This is similar to {@link #setDisplay(SurfaceHolder)}, but
+ * does not support {@link #setScreenOnWhilePlaying(boolean)}. Setting a
+ * Surface will un-set any Surface or SurfaceHolder that was previously set.
+ * A null surface will result in only the audio track being played.
+ *
+ * If the Surface sends frames to a {@link SurfaceTexture}, the timestamps
+ * returned from {@link SurfaceTexture#getTimestamp()} will have an
+ * unspecified zero point. These timestamps cannot be directly compared
+ * between different media sources, different instances of the same media
+ * source, or multiple runs of the same program. The timestamp is normally
+ * monotonically increasing and is unaffected by time-of-day adjustments,
+ * but it is reset when the position is set.
+ *
+ * @param surface The {@link Surface} to be used for the video portion of
+ * the media.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ */
+ @Override
+ public void setSurface(Surface surface) {
+ if (mScreenOnWhilePlaying && surface != null) {
+ Log.w(TAG, "setScreenOnWhilePlaying(true) is ineffective for Surface");
+ }
+ mSurfaceHolder = null;
+ _setVideoSurface(surface);
+ updateSurfaceScreenOn();
+ }
+
+ /**
+ * Sets video scaling mode. To make the target video scaling mode
+ * effective during playback, this method must be called after
+ * data source is set. If not called, the default video
+ * scaling mode is {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT}.
+ *
+ * <p> The supported video scaling modes are:
+ * <ul>
+ * <li> {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT}
+ * <li> {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}
+ * </ul>
+ *
+ * @param mode target video scaling mode. Must be one of the supported
+ * video scaling modes; otherwise, IllegalArgumentException will be thrown.
+ *
+ * @see MediaPlayer2#VIDEO_SCALING_MODE_SCALE_TO_FIT
+ * @see MediaPlayer2#VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
+ * @hide
+ */
+ @Override
+ public void setVideoScalingMode(int mode) {
+ if (!isVideoScalingModeSupported(mode)) {
+ final String msg = "Scaling mode " + mode + " is not supported";
+ throw new IllegalArgumentException(msg);
+ }
+ Parcel request = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ try {
+ request.writeInt(INVOKE_ID_SET_VIDEO_SCALE_MODE);
+ request.writeInt(mode);
+ invoke(request, reply);
+ } finally {
+ request.recycle();
+ reply.recycle();
+ }
+ }
+
+ /**
+ * Discards all pending commands.
+ */
+ @Override
+ public void clearPendingCommands() {
+ }
+
+ /**
+ * Sets the data source as described by a DataSourceDesc.
+ *
+ * @param dsd the descriptor of data source you want to play
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws NullPointerException if dsd is null
+ */
+ @Override
+ public void setDataSource(@NonNull DataSourceDesc dsd) throws IOException {
+ Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+ mPlaylist = Collections.synchronizedList(new ArrayList<DataSourceDesc>(1));
+ mPlaylist.add(dsd);
+ mPLCurrentIndex = 0;
+ setDataSourcePriv(dsd);
+ }
+
+ /**
+ * Gets the current data source as described by a DataSourceDesc.
+ *
+ * @return the current DataSourceDesc
+ */
+ @Override
+ public DataSourceDesc getCurrentDataSource() {
+ if (mPlaylist == null) {
+ return null;
+ }
+ return mPlaylist.get(mPLCurrentIndex);
+ }
+
+ /**
+ * Sets the play list.
+ *
+ * If startIndex falls outside play list range, it will be clamped to the nearest index
+ * in the play list.
+ *
+ * @param pl the play list of data source you want to play
+ * @param startIndex the index of the DataSourceDesc in the play list you want to play first
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if pl is null or empty, or pl contains null DataSourceDesc
+ */
+ @Override
+ public void setPlaylist(@NonNull List<DataSourceDesc> pl, int startIndex)
+ throws IOException {
+ if (pl == null || pl.size() == 0) {
+ throw new IllegalArgumentException("play list cannot be null or empty.");
+ }
+ HashSet ids = new HashSet(pl.size());
+ for (DataSourceDesc dsd : pl) {
+ if (dsd == null) {
+ throw new IllegalArgumentException("DataSourceDesc in play list cannot be null.");
+ }
+ if (ids.add(dsd.getId()) == false) {
+ throw new IllegalArgumentException("DataSourceDesc Id in play list should be unique.");
+ }
+ }
+
+ if (startIndex < 0) {
+ startIndex = 0;
+ } else if (startIndex >= pl.size()) {
+ startIndex = pl.size() - 1;
+ }
+
+ mPlaylist = Collections.synchronizedList(new ArrayList(pl));
+ mPLCurrentIndex = startIndex;
+ setDataSourcePriv(mPlaylist.get(startIndex));
+ // TODO: handle the preparation of next source in the play list.
+ // It should be processed after current source is prepared.
+ }
+
+ /**
+ * Gets a copy of the play list.
+ *
+ * @return a copy of the play list used by {@link MediaPlayer2}
+ */
+ @Override
+ public List<DataSourceDesc> getPlaylist() {
+ if (mPlaylist == null) {
+ return null;
+ }
+ return new ArrayList(mPlaylist);
+ }
+
+ /**
+ * Sets the index of current DataSourceDesc in the play list to be played.
+ *
+ * @param index the index of DataSourceDesc in the play list you want to play
+ * @throws IllegalArgumentException if the play list is null
+ * @throws NullPointerException if index is outside play list range
+ */
+ @Override
+ public void setCurrentPlaylistItem(int index) {
+ if (mPlaylist == null) {
+ throw new IllegalArgumentException("play list has not been set yet.");
+ }
+ if (index < 0 || index >= mPlaylist.size()) {
+ throw new IndexOutOfBoundsException("index is out of play list range.");
+ }
+
+ if (index == mPLCurrentIndex) {
+ return;
+ }
+
+ // TODO: in playing state, stop current source and start to play source of index.
+ mPLCurrentIndex = index;
+ }
+
+ /**
+ * Sets the index of next-to-be-played DataSourceDesc in the play list.
+ *
+ * @param index the index of next-to-be-played DataSourceDesc in the play list
+ * @throws IllegalArgumentException if the play list is null
+ * @throws NullPointerException if index is outside play list range
+ */
+ @Override
+ public void setNextPlaylistItem(int index) {
+ if (mPlaylist == null) {
+ throw new IllegalArgumentException("play list has not been set yet.");
+ }
+ if (index < 0 || index >= mPlaylist.size()) {
+ throw new IndexOutOfBoundsException("index is out of play list range.");
+ }
+
+ if (index == mPLNextIndex) {
+ return;
+ }
+
+ // TODO: prepare the new next-to-be-played DataSourceDesc
+ mPLNextIndex = index;
+ }
+
+ /**
+ * Gets the current index of play list.
+ *
+ * @return the index of the current DataSourceDesc in the play list
+ */
+ @Override
+ public int getCurrentPlaylistItemIndex() {
+ return mPLCurrentIndex;
+ }
+
+ /**
+ * Sets the looping mode of the play list.
+ * The mode shall be one of {@link #LOOPING_MODE_NONE}, {@link #LOOPING_MODE_FULL},
+ * {@link #LOOPING_MODE_SINGLE}, {@link #LOOPING_MODE_SHUFFLE}.
+ *
+ * @param mode the mode in which the play list will be played
+ * @throws IllegalArgumentException if mode is not supported
+ */
+ @Override
+ public void setLoopingMode(@LoopingMode int mode) {
+ if (mode != LOOPING_MODE_NONE
+ && mode != LOOPING_MODE_FULL
+ && mode != LOOPING_MODE_SINGLE
+ && mode != LOOPING_MODE_SHUFFLE) {
+ throw new IllegalArgumentException("mode is not supported.");
+ }
+ mLoopingMode = mode;
+ if (mPlaylist == null) {
+ return;
+ }
+
+ // TODO: handle the new mode if necessary.
+ }
+
+ /**
+ * Gets the looping mode of play list.
+ *
+ * @return the looping mode of the play list
+ */
+ @Override
+ public int getLoopingMode() {
+ return mPLCurrentIndex;
+ }
+
+ /**
+ * Moves the DataSourceDesc at indexFrom in the play list to indexTo.
+ *
+ * @throws IllegalArgumentException if the play list is null
+ * @throws IndexOutOfBoundsException if indexFrom or indexTo is outside play list range
+ */
+ @Override
+ public void movePlaylistItem(int indexFrom, int indexTo) {
+ if (mPlaylist == null) {
+ throw new IllegalArgumentException("play list has not been set yet.");
+ }
+ // TODO: move the DataSourceDesc from indexFrom to indexTo.
+ }
+
+ /**
+ * Removes the DataSourceDesc at index in the play list.
+ *
+ * If index is same as the current index of the play list, current DataSourceDesc
+ * will be stopped and playback moves to next source in the list.
+ *
+ * @return the removed DataSourceDesc at index in the play list
+ * @throws IllegalArgumentException if the play list is null
+ * @throws IndexOutOfBoundsException if index is outside play list range
+ */
+ @Override
+ public DataSourceDesc removePlaylistItem(int index) {
+ if (mPlaylist == null) {
+ throw new IllegalArgumentException("play list has not been set yet.");
+ }
+
+ DataSourceDesc oldDsd = mPlaylist.remove(index);
+ // TODO: if index == mPLCurrentIndex, stop current source and move to next one.
+ // if index == mPLNextIndex, prepare the new next-to-be-played source.
+ return oldDsd;
+ }
+
+ /**
+ * Inserts the DataSourceDesc to the play list at position index.
+ *
+ * This will not change the DataSourceDesc currently being played.
+ * If index is less than or equal to the current index of the play list,
+ * the current index of the play list will be incremented correspondingly.
+ *
+ * @param index the index you want to add dsd to the play list
+ * @param dsd the descriptor of data source you want to add to the play list
+ * @throws IndexOutOfBoundsException if index is outside play list range
+ * @throws NullPointerException if dsd is null
+ */
+ @Override
+ public void addPlaylistItem(int index, DataSourceDesc dsd) {
+ Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+
+ if (mPlaylist == null) {
+ if (index == 0) {
+ mPlaylist = Collections.synchronizedList(new ArrayList<DataSourceDesc>());
+ mPlaylist.add(dsd);
+ mPLCurrentIndex = 0;
+ return;
+ }
+ throw new IllegalArgumentException("index should be 0 for first DataSourceDesc.");
+ }
+
+ long id = dsd.getId();
+ for (DataSourceDesc pldsd : mPlaylist) {
+ if (id == pldsd.getId()) {
+ throw new IllegalArgumentException("Id of dsd already exists in the play list.");
+ }
+ }
+
+ mPlaylist.add(index, dsd);
+ if (index <= mPLCurrentIndex) {
+ ++mPLCurrentIndex;
+ }
+ }
+
+ /**
+ * replaces the DataSourceDesc at index in the play list with given dsd.
+ *
+ * When index is same as the current index of the play list, the current source
+ * will be stopped and the new source will be played, except that if new
+ * and old source only differ on end position and current media position is
+ * smaller then the new end position.
+ *
+ * This will not change the DataSourceDesc currently being played.
+ * If index is less than or equal to the current index of the play list,
+ * the current index of the play list will be incremented correspondingly.
+ *
+ * @param index the index you want to add dsd to the play list
+ * @param dsd the descriptor of data source you want to add to the play list
+ * @throws IndexOutOfBoundsException if index is outside play list range
+ * @throws NullPointerException if dsd is null
+ */
+ @Override
+ public DataSourceDesc editPlaylistItem(int index, DataSourceDesc dsd) {
+ Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+ Preconditions.checkNotNull(mPlaylist, "the play list cannot be null");
+
+ long id = dsd.getId();
+ for (int i = 0; i < mPlaylist.size(); ++i) {
+ if (i == index) {
+ continue;
+ }
+ if (id == mPlaylist.get(i).getId()) {
+ throw new IllegalArgumentException("Id of dsd already exists in the play list.");
+ }
+ }
+
+ // TODO: if needed, stop playback of current source, and start new dsd.
+ DataSourceDesc oldDsd = mPlaylist.set(index, dsd);
+ return mPlaylist.set(index, dsd);
+ }
+
+ private void setDataSourcePriv(@NonNull DataSourceDesc dsd) throws IOException {
+ Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+
+ switch (dsd.getType()) {
+ case DataSourceDesc.TYPE_CALLBACK:
+ setDataSourcePriv(dsd.getId(),
+ dsd.getMedia2DataSource());
+ break;
+
+ case DataSourceDesc.TYPE_FD:
+ setDataSourcePriv(dsd.getId(),
+ dsd.getFileDescriptor(),
+ dsd.getFileDescriptorOffset(),
+ dsd.getFileDescriptorLength());
+ break;
+
+ case DataSourceDesc.TYPE_URI:
+ setDataSourcePriv(dsd.getId(),
+ dsd.getUriContext(),
+ dsd.getUri(),
+ dsd.getUriHeaders(),
+ dsd.getUriCookies());
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ /**
+ * To provide cookies for the subsequent HTTP requests, you can install your own default cookie
+ * handler and use other variants of setDataSource APIs instead. Alternatively, you can use
+ * this API to pass the cookies as a list of HttpCookie. If the app has not installed
+ * a CookieHandler already, this API creates a CookieManager and populates its CookieStore with
+ * the provided cookies. If the app has installed its own handler already, this API requires the
+ * handler to be of CookieManager type such that the API can update the manager’s CookieStore.
+ *
+ * <p><strong>Note</strong> that the cross domain redirection is allowed by default,
+ * but that can be changed with key/value pairs through the headers parameter with
+ * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value to
+ * disallow or allow cross domain redirection.
+ *
+ * @throws IllegalArgumentException if cookies are provided and the installed handler is not
+ * a CookieManager
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws NullPointerException if context or uri is null
+ * @throws IOException if uri has a file scheme and an I/O error occurs
+ */
+ private void setDataSourcePriv(long srcId, @NonNull Context context, @NonNull Uri uri,
+ @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies)
+ throws IOException {
+ if (context == null) {
+ throw new NullPointerException("context param can not be null.");
+ }
+
+ if (uri == null) {
+ throw new NullPointerException("uri param can not be null.");
+ }
+
+ if (cookies != null) {
+ CookieHandler cookieHandler = CookieHandler.getDefault();
+ if (cookieHandler != null && !(cookieHandler instanceof CookieManager)) {
+ throw new IllegalArgumentException("The cookie handler has to be of CookieManager "
+ + "type when cookies are provided.");
+ }
+ }
+
+ // The context and URI usually belong to the calling user. Get a resolver for that user
+ // and strip out the userId from the URI if present.
+ final ContentResolver resolver = context.getContentResolver();
+ final String scheme = uri.getScheme();
+ final String authority = ContentProvider.getAuthorityWithoutUserId(uri.getAuthority());
+ if (ContentResolver.SCHEME_FILE.equals(scheme)) {
+ setDataSourcePriv(srcId, uri.getPath(), null, null);
+ return;
+ } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)
+ && Settings.AUTHORITY.equals(authority)) {
+ // Try cached ringtone first since the actual provider may not be
+ // encryption aware, or it may be stored on CE media storage
+ final int type = RingtoneManager.getDefaultType(uri);
+ final Uri cacheUri = RingtoneManager.getCacheForType(type, context.getUserId());
+ final Uri actualUri = RingtoneManager.getActualDefaultRingtoneUri(context, type);
+ if (attemptDataSource(srcId, resolver, cacheUri)) {
+ return;
+ } else if (attemptDataSource(srcId, resolver, actualUri)) {
+ return;
+ } else {
+ setDataSourcePriv(srcId, uri.toString(), headers, cookies);
+ }
+ } else {
+ // Try requested Uri locally first, or fallback to media server
+ if (attemptDataSource(srcId, resolver, uri)) {
+ return;
+ } else {
+ setDataSourcePriv(srcId, uri.toString(), headers, cookies);
+ }
+ }
+ }
+
+ private boolean attemptDataSource(long srcId, ContentResolver resolver, Uri uri) {
+ try (AssetFileDescriptor afd = resolver.openAssetFileDescriptor(uri, "r")) {
+ if (afd.getDeclaredLength() < 0) {
+ setDataSourcePriv(srcId, afd.getFileDescriptor(), 0, DataSourceDesc.LONG_MAX);
+ } else {
+ setDataSourcePriv(srcId,
+ afd.getFileDescriptor(),
+ afd.getStartOffset(),
+ afd.getDeclaredLength());
+ }
+ return true;
+ } catch (NullPointerException | SecurityException | IOException ex) {
+ Log.w(TAG, "Couldn't open " + uri + ": " + ex);
+ return false;
+ }
+ }
+
+ private void setDataSourcePriv(
+ long srcId, String path, Map<String, String> headers, List<HttpCookie> cookies)
+ throws IOException, IllegalArgumentException, SecurityException, IllegalStateException
+ {
+ String[] keys = null;
+ String[] values = null;
+
+ if (headers != null) {
+ keys = new String[headers.size()];
+ values = new String[headers.size()];
+
+ int i = 0;
+ for (Map.Entry<String, String> entry: headers.entrySet()) {
+ keys[i] = entry.getKey();
+ values[i] = entry.getValue();
+ ++i;
+ }
+ }
+ setDataSourcePriv(srcId, path, keys, values, cookies);
+ }
+
+ private void setDataSourcePriv(long srcId, String path, String[] keys, String[] values,
+ List<HttpCookie> cookies)
+ throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
+ final Uri uri = Uri.parse(path);
+ final String scheme = uri.getScheme();
+ if ("file".equals(scheme)) {
+ path = uri.getPath();
+ } else if (scheme != null) {
+ // handle non-file sources
+ nativeSetDataSource(
+ Media2HTTPService.createHTTPService(path, cookies),
+ path,
+ keys,
+ values);
+ return;
+ }
+
+ final File file = new File(path);
+ if (file.exists()) {
+ FileInputStream is = new FileInputStream(file);
+ FileDescriptor fd = is.getFD();
+ setDataSourcePriv(srcId, fd, 0, DataSourceDesc.LONG_MAX);
+ is.close();
+ } else {
+ throw new IOException("setDataSourcePriv failed.");
+ }
+ }
+
+ private native void nativeSetDataSource(
+ Media2HTTPService httpService, String path, String[] keys, String[] values)
+ throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;
+
+ /**
+ * Sets the data source (FileDescriptor) to use. The FileDescriptor must be
+ * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility
+ * to close the file descriptor. It is safe to do so as soon as this call returns.
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if fd is not a valid FileDescriptor
+ * @throws IOException if fd can not be read
+ */
+ private void setDataSourcePriv(long srcId, FileDescriptor fd, long offset, long length)
+ throws IOException {
+ _setDataSource(fd, offset, length);
+ }
+
+ private native void _setDataSource(FileDescriptor fd, long offset, long length)
+ throws IOException;
+
+ /**
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if dataSource is not a valid Media2DataSource
+ */
+ private void setDataSourcePriv(long srcId, Media2DataSource dataSource) {
+ _setDataSource(dataSource);
+ }
+
+ private native void _setDataSource(Media2DataSource dataSource);
+
+ /**
+ * Prepares the player for playback, synchronously.
+ *
+ * After setting the datasource and the display surface, you need to either
+ * call prepare() or prepareAsync(). For files, it is OK to call prepare(),
+ * which blocks until MediaPlayer2 is ready for playback.
+ *
+ * @throws IOException if source can not be accessed
+ * @throws IllegalStateException if it is called in an invalid state
+ * @hide
+ */
+ @Override
+ public void prepare() throws IOException {
+ _prepare();
+ scanInternalSubtitleTracks();
+
+ // DrmInfo, if any, has been resolved by now.
+ synchronized (mDrmLock) {
+ mDrmInfoResolved = true;
+ }
+ }
+
+ private native void _prepare() throws IOException, IllegalStateException;
+
+ /**
+ * Prepares the player for playback, asynchronously.
+ *
+ * After setting the datasource and the display surface, you need to either
+ * call prepare() or prepareAsync(). For streams, you should call prepareAsync(),
+ * which returns immediately, rather than blocking until enough data has been
+ * buffered.
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ @Override
+ public native void prepareAsync();
+
+ /**
+ * Starts or resumes playback. If playback had previously been paused,
+ * playback will continue from where it was paused. If playback had
+ * been stopped, or never started before, playback will start at the
+ * beginning.
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ @Override
+ public void play() {
+ stayAwake(true);
+ _start();
+ }
+
+ private native void _start() throws IllegalStateException;
+
+
+ private int getAudioStreamType() {
+ if (mStreamType == AudioManager.USE_DEFAULT_STREAM_TYPE) {
+ mStreamType = _getAudioStreamType();
+ }
+ return mStreamType;
+ }
+
+ private native int _getAudioStreamType() throws IllegalStateException;
+
+ /**
+ * Stops playback after playback has been started or paused.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ * #hide
+ */
+ @Override
+ public void stop() {
+ stayAwake(false);
+ _stop();
+ }
+
+ private native void _stop() throws IllegalStateException;
+
+ /**
+ * Pauses playback. Call play() to resume.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ @Override
+ public void pause() {
+ stayAwake(false);
+ _pause();
+ }
+
+ private native void _pause() throws IllegalStateException;
+
+ //--------------------------------------------------------------------------
+ // Explicit Routing
+ //--------------------
+ private AudioDeviceInfo mPreferredDevice = null;
+
+ /**
+ * Specifies an audio device (via an {@link AudioDeviceInfo} object) to route
+ * the output from this MediaPlayer2.
+ * @param deviceInfo The {@link AudioDeviceInfo} specifying the audio sink or source.
+ * If deviceInfo is null, default routing is restored.
+ * @return true if succesful, false if the specified {@link AudioDeviceInfo} is non-null and
+ * does not correspond to a valid audio device.
+ */
+ @Override
+ public boolean setPreferredDevice(AudioDeviceInfo deviceInfo) {
+ if (deviceInfo != null && !deviceInfo.isSink()) {
+ return false;
+ }
+ int preferredDeviceId = deviceInfo != null ? deviceInfo.getId() : 0;
+ boolean status = native_setOutputDevice(preferredDeviceId);
+ if (status == true) {
+ synchronized (this) {
+ mPreferredDevice = deviceInfo;
+ }
+ }
+ return status;
+ }
+
+ /**
+ * Returns the selected output specified by {@link #setPreferredDevice}. Note that this
+ * is not guaranteed to correspond to the actual device being used for playback.
+ */
+ @Override
+ public AudioDeviceInfo getPreferredDevice() {
+ synchronized (this) {
+ return mPreferredDevice;
+ }
+ }
+
+ /**
+ * Returns an {@link AudioDeviceInfo} identifying the current routing of this MediaPlayer2
+ * Note: The query is only valid if the MediaPlayer2 is currently playing.
+ * If the player is not playing, the returned device can be null or correspond to previously
+ * selected device when the player was last active.
+ */
+ @Override
+ public AudioDeviceInfo getRoutedDevice() {
+ int deviceId = native_getRoutedDeviceId();
+ if (deviceId == 0) {
+ return null;
+ }
+ AudioDeviceInfo[] devices =
+ AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_OUTPUTS);
+ for (int i = 0; i < devices.length; i++) {
+ if (devices[i].getId() == deviceId) {
+ return devices[i];
+ }
+ }
+ return null;
+ }
+
+ /*
+ * Call BEFORE adding a routing callback handler or AFTER removing a routing callback handler.
+ */
+ private void enableNativeRoutingCallbacksLocked(boolean enabled) {
+ if (mRoutingChangeListeners.size() == 0) {
+ native_enableDeviceCallback(enabled);
+ }
+ }
+
+ /**
+ * The list of AudioRouting.OnRoutingChangedListener interfaces added (with
+ * {@link #addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, Handler)}
+ * by an app to receive (re)routing notifications.
+ */
+ @GuardedBy("mRoutingChangeListeners")
+ private ArrayMap<AudioRouting.OnRoutingChangedListener,
+ NativeRoutingEventHandlerDelegate> mRoutingChangeListeners = new ArrayMap<>();
+
+ /**
+ * Adds an {@link AudioRouting.OnRoutingChangedListener} to receive notifications of routing
+ * changes on this MediaPlayer2.
+ * @param listener The {@link AudioRouting.OnRoutingChangedListener} interface to receive
+ * notifications of rerouting events.
+ * @param handler Specifies the {@link Handler} object for the thread on which to execute
+ * the callback. If <code>null</code>, the handler on the main looper will be used.
+ */
+ @Override
+ public void addOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener,
+ Handler handler) {
+ synchronized (mRoutingChangeListeners) {
+ if (listener != null && !mRoutingChangeListeners.containsKey(listener)) {
+ enableNativeRoutingCallbacksLocked(true);
+ mRoutingChangeListeners.put(
+ listener, new NativeRoutingEventHandlerDelegate(this, listener,
+ handler != null ? handler : mEventHandler));
+ }
+ }
+ }
+
+ /**
+ * Removes an {@link AudioRouting.OnRoutingChangedListener} which has been previously added
+ * to receive rerouting notifications.
+ * @param listener The previously added {@link AudioRouting.OnRoutingChangedListener} interface
+ * to remove.
+ */
+ @Override
+ public void removeOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener) {
+ synchronized (mRoutingChangeListeners) {
+ if (mRoutingChangeListeners.containsKey(listener)) {
+ mRoutingChangeListeners.remove(listener);
+ enableNativeRoutingCallbacksLocked(false);
+ }
+ }
+ }
+
+ private native final boolean native_setOutputDevice(int deviceId);
+ private native final int native_getRoutedDeviceId();
+ private native final void native_enableDeviceCallback(boolean enabled);
+
+ /**
+ * Set the low-level power management behavior for this MediaPlayer2. This
+ * can be used when the MediaPlayer2 is not playing through a SurfaceHolder
+ * set with {@link #setDisplay(SurfaceHolder)} and thus can use the
+ * high-level {@link #setScreenOnWhilePlaying(boolean)} feature.
+ *
+ * <p>This function has the MediaPlayer2 access the low-level power manager
+ * service to control the device's power usage while playing is occurring.
+ * The parameter is a combination of {@link android.os.PowerManager} wake flags.
+ * Use of this method requires {@link android.Manifest.permission#WAKE_LOCK}
+ * permission.
+ * By default, no attempt is made to keep the device awake during playback.
+ *
+ * @param context the Context to use
+ * @param mode the power/wake mode to set
+ * @see android.os.PowerManager
+ * @hide
+ */
+ @Override
+ public void setWakeMode(Context context, int mode) {
+ boolean washeld = false;
+
+ /* Disable persistant wakelocks in media player based on property */
+ if (SystemProperties.getBoolean("audio.offload.ignore_setawake", false) == true) {
+ Log.w(TAG, "IGNORING setWakeMode " + mode);
+ return;
+ }
+
+ if (mWakeLock != null) {
+ if (mWakeLock.isHeld()) {
+ washeld = true;
+ mWakeLock.release();
+ }
+ mWakeLock = null;
+ }
+
+ PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(mode|PowerManager.ON_AFTER_RELEASE, MediaPlayer2Impl.class.getName());
+ mWakeLock.setReferenceCounted(false);
+ if (washeld) {
+ mWakeLock.acquire();
+ }
+ }
+
+ /**
+ * Control whether we should use the attached SurfaceHolder to keep the
+ * screen on while video playback is occurring. This is the preferred
+ * method over {@link #setWakeMode} where possible, since it doesn't
+ * require that the application have permission for low-level wake lock
+ * access.
+ *
+ * @param screenOn Supply true to keep the screen on, false to allow it
+ * to turn off.
+ * @hide
+ */
+ @Override
+ public void setScreenOnWhilePlaying(boolean screenOn) {
+ if (mScreenOnWhilePlaying != screenOn) {
+ if (screenOn && mSurfaceHolder == null) {
+ Log.w(TAG, "setScreenOnWhilePlaying(true) is ineffective without a SurfaceHolder");
+ }
+ mScreenOnWhilePlaying = screenOn;
+ updateSurfaceScreenOn();
+ }
+ }
+
+ private void stayAwake(boolean awake) {
+ if (mWakeLock != null) {
+ if (awake && !mWakeLock.isHeld()) {
+ mWakeLock.acquire();
+ } else if (!awake && mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
+ }
+ mStayAwake = awake;
+ updateSurfaceScreenOn();
+ }
+
+ private void updateSurfaceScreenOn() {
+ if (mSurfaceHolder != null) {
+ mSurfaceHolder.setKeepScreenOn(mScreenOnWhilePlaying && mStayAwake);
+ }
+ }
+
+ /**
+ * Returns the width of the video.
+ *
+ * @return the width of the video, or 0 if there is no video,
+ * no display surface was set, or the width has not been determined
+ * yet. The {@code EventCallback} can be registered via
+ * {@link #registerEventCallback(Executor, EventCallback)} to provide a
+ * notification {@code EventCallback.onVideoSizeChanged} when the width is available.
+ */
+ @Override
+ public native int getVideoWidth();
+
+ /**
+ * Returns the height of the video.
+ *
+ * @return the height of the video, or 0 if there is no video,
+ * no display surface was set, or the height has not been determined
+ * yet. The {@code EventCallback} can be registered via
+ * {@link #registerEventCallback(Executor, EventCallback)} to provide a
+ * notification {@code EventCallback.onVideoSizeChanged} when the height is available.
+ */
+ @Override
+ public native int getVideoHeight();
+
+ /**
+ * Return Metrics data about the current player.
+ *
+ * @return a {@link PersistableBundle} containing the set of attributes and values
+ * available for the media being handled by this instance of MediaPlayer2
+ * The attributes are descibed in {@link MetricsConstants}.
+ *
+ * Additional vendor-specific fields may also be present in
+ * the return value.
+ */
+ @Override
+ public PersistableBundle getMetrics() {
+ PersistableBundle bundle = native_getMetrics();
+ return bundle;
+ }
+
+ private native PersistableBundle native_getMetrics();
+
+ /**
+ * Checks whether the MediaPlayer2 is playing.
+ *
+ * @return true if currently playing, false otherwise
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ */
+ @Override
+ public native boolean isPlaying();
+
+ /**
+ * Gets the current buffering management params used by the source component.
+ * Calling it only after {@code setDataSource} has been called.
+ * Each type of data source might have different set of default params.
+ *
+ * @return the current buffering management params used by the source component.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized, or {@code setDataSource} has not been called.
+ * @hide
+ */
+ @Override
+ @NonNull
+ public native BufferingParams getBufferingParams();
+
+ /**
+ * Sets buffering management params.
+ * The object sets its internal BufferingParams to the input, except that the input is
+ * invalid or not supported.
+ * Call it only after {@code setDataSource} has been called.
+ * The input is a hint to MediaPlayer2.
+ *
+ * @param params the buffering management params.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released, or {@code setDataSource} has not been called.
+ * @throws IllegalArgumentException if params is invalid or not supported.
+ * @hide
+ */
+ @Override
+ public native void setBufferingParams(@NonNull BufferingParams params);
+
+ /**
+ * Sets playback rate and audio mode.
+ *
+ * @param rate the ratio between desired playback rate and normal one.
+ * @param audioMode audio playback mode. Must be one of the supported
+ * audio modes.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ * @throws IllegalArgumentException if audioMode is not supported.
+ *
+ * @hide
+ */
+ @Override
+ @NonNull
+ public PlaybackParams easyPlaybackParams(float rate, @PlaybackRateAudioMode int audioMode) {
+ PlaybackParams params = new PlaybackParams();
+ params.allowDefaults();
+ switch (audioMode) {
+ case PLAYBACK_RATE_AUDIO_MODE_DEFAULT:
+ params.setSpeed(rate).setPitch(1.0f);
+ break;
+ case PLAYBACK_RATE_AUDIO_MODE_STRETCH:
+ params.setSpeed(rate).setPitch(1.0f)
+ .setAudioFallbackMode(params.AUDIO_FALLBACK_MODE_FAIL);
+ break;
+ case PLAYBACK_RATE_AUDIO_MODE_RESAMPLE:
+ params.setSpeed(rate).setPitch(rate);
+ break;
+ default:
+ final String msg = "Audio playback mode " + audioMode + " is not supported";
+ throw new IllegalArgumentException(msg);
+ }
+ return params;
+ }
+
+ /**
+ * Sets playback rate using {@link PlaybackParams}. The object sets its internal
+ * PlaybackParams to the input, except that the object remembers previous speed
+ * when input speed is zero. This allows the object to resume at previous speed
+ * when play() is called. Calling it before the object is prepared does not change
+ * the object state. After the object is prepared, calling it with zero speed is
+ * equivalent to calling pause(). After the object is prepared, calling it with
+ * non-zero speed is equivalent to calling play().
+ *
+ * @param params the playback params.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ * @throws IllegalArgumentException if params is not supported.
+ */
+ @Override
+ public native void setPlaybackParams(@NonNull PlaybackParams params);
+
+ /**
+ * Gets the playback params, containing the current playback rate.
+ *
+ * @return the playback params.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ @Override
+ @NonNull
+ public native PlaybackParams getPlaybackParams();
+
+ /**
+ * Sets A/V sync mode.
+ *
+ * @param params the A/V sync params to apply
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ * @throws IllegalArgumentException if params are not supported.
+ */
+ @Override
+ public native void setSyncParams(@NonNull SyncParams params);
+
+ /**
+ * Gets the A/V sync mode.
+ *
+ * @return the A/V sync params
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ @Override
+ @NonNull
+ public native SyncParams getSyncParams();
+
+ private native final void _seekTo(long msec, int mode);
+
+ /**
+ * Moves the media to specified time position by considering the given mode.
+ * <p>
+ * When seekTo is finished, the user will be notified via OnSeekComplete supplied by the user.
+ * There is at most one active seekTo processed at any time. If there is a to-be-completed
+ * seekTo, new seekTo requests will be queued in such a way that only the last request
+ * is kept. When current seekTo is completed, the queued request will be processed if
+ * that request is different from just-finished seekTo operation, i.e., the requested
+ * position or mode is different.
+ *
+ * @param msec the offset in milliseconds from the start to seek to.
+ * When seeking to the given time position, there is no guarantee that the data source
+ * has a frame located at the position. When this happens, a frame nearby will be rendered.
+ * If msec is negative, time position zero will be used.
+ * If msec is larger than duration, duration will be used.
+ * @param mode the mode indicating where exactly to seek to.
+ * Use {@link #SEEK_PREVIOUS_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp earlier than or the same as msec. Use
+ * {@link #SEEK_NEXT_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp later than or the same as msec. Use
+ * {@link #SEEK_CLOSEST_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp closest to or the same as msec. Use
+ * {@link #SEEK_CLOSEST} if one wants to seek to a frame that may
+ * or may not be a sync frame but is closest to or the same as msec.
+ * {@link #SEEK_CLOSEST} often has larger performance overhead compared
+ * to the other options if there is no sync frame located at msec.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized
+ * @throws IllegalArgumentException if the mode is invalid.
+ */
+ @Override
+ public void seekTo(long msec, @SeekMode int mode) {
+ if (mode < SEEK_PREVIOUS_SYNC || mode > SEEK_CLOSEST) {
+ final String msg = "Illegal seek mode: " + mode;
+ throw new IllegalArgumentException(msg);
+ }
+ // TODO: pass long to native, instead of truncating here.
+ if (msec > Integer.MAX_VALUE) {
+ Log.w(TAG, "seekTo offset " + msec + " is too large, cap to " + Integer.MAX_VALUE);
+ msec = Integer.MAX_VALUE;
+ } else if (msec < Integer.MIN_VALUE) {
+ Log.w(TAG, "seekTo offset " + msec + " is too small, cap to " + Integer.MIN_VALUE);
+ msec = Integer.MIN_VALUE;
+ }
+ _seekTo(msec, mode);
+ }
+
+ /**
+ * Get current playback position as a {@link MediaTimestamp}.
+ * <p>
+ * The MediaTimestamp represents how the media time correlates to the system time in
+ * a linear fashion using an anchor and a clock rate. During regular playback, the media
+ * time moves fairly constantly (though the anchor frame may be rebased to a current
+ * system time, the linear correlation stays steady). Therefore, this method does not
+ * need to be called often.
+ * <p>
+ * To help users get current playback position, this method always anchors the timestamp
+ * to the current {@link System#nanoTime system time}, so
+ * {@link MediaTimestamp#getAnchorMediaTimeUs} can be used as current playback position.
+ *
+ * @return a MediaTimestamp object if a timestamp is available, or {@code null} if no timestamp
+ * is available, e.g. because the media player has not been initialized.
+ *
+ * @see MediaTimestamp
+ */
+ @Override
+ @Nullable
+ public MediaTimestamp getTimestamp()
+ {
+ try {
+ // TODO: get the timestamp from native side
+ return new MediaTimestamp(
+ getCurrentPosition() * 1000L,
+ System.nanoTime(),
+ isPlaying() ? getPlaybackParams().getSpeed() : 0.f);
+ } catch (IllegalStateException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the current playback position.
+ *
+ * @return the current position in milliseconds
+ */
+ @Override
+ public native int getCurrentPosition();
+
+ /**
+ * Gets the duration of the file.
+ *
+ * @return the duration in milliseconds, if no duration is available
+ * (for example, if streaming live content), -1 is returned.
+ */
+ @Override
+ public native int getDuration();
+
+ /**
+ * Gets the media metadata.
+ *
+ * @param update_only controls whether the full set of available
+ * metadata is returned or just the set that changed since the
+ * last call. See {@see #METADATA_UPDATE_ONLY} and {@see
+ * #METADATA_ALL}.
+ *
+ * @param apply_filter if true only metadata that matches the
+ * filter is returned. See {@see #APPLY_METADATA_FILTER} and {@see
+ * #BYPASS_METADATA_FILTER}.
+ *
+ * @return The metadata, possibly empty. null if an error occured.
+ // FIXME: unhide.
+ * {@hide}
+ */
+ @Override
+ public Metadata getMetadata(final boolean update_only,
+ final boolean apply_filter) {
+ Parcel reply = Parcel.obtain();
+ Metadata data = new Metadata();
+
+ if (!native_getMetadata(update_only, apply_filter, reply)) {
+ reply.recycle();
+ return null;
+ }
+
+ // Metadata takes over the parcel, don't recycle it unless
+ // there is an error.
+ if (!data.parse(reply)) {
+ reply.recycle();
+ return null;
+ }
+ return data;
+ }
+
+ /**
+ * Set a filter for the metadata update notification and update
+ * retrieval. The caller provides 2 set of metadata keys, allowed
+ * and blocked. The blocked set always takes precedence over the
+ * allowed one.
+ * Metadata.MATCH_ALL and Metadata.MATCH_NONE are 2 sets available as
+ * shorthands to allow/block all or no metadata.
+ *
+ * By default, there is no filter set.
+ *
+ * @param allow Is the set of metadata the client is interested
+ * in receiving new notifications for.
+ * @param block Is the set of metadata the client is not interested
+ * in receiving new notifications for.
+ * @return The call status code.
+ *
+ // FIXME: unhide.
+ * {@hide}
+ */
+ @Override
+ public int setMetadataFilter(Set<Integer> allow, Set<Integer> block) {
+ // Do our serialization manually instead of calling
+ // Parcel.writeArray since the sets are made of the same type
+ // we avoid paying the price of calling writeValue (used by
+ // writeArray) which burns an extra int per element to encode
+ // the type.
+ Parcel request = newRequest();
+
+ // The parcel starts already with an interface token. There
+ // are 2 filters. Each one starts with a 4bytes number to
+ // store the len followed by a number of int (4 bytes as well)
+ // representing the metadata type.
+ int capacity = request.dataSize() + 4 * (1 + allow.size() + 1 + block.size());
+
+ if (request.dataCapacity() < capacity) {
+ request.setDataCapacity(capacity);
+ }
+
+ request.writeInt(allow.size());
+ for(Integer t: allow) {
+ request.writeInt(t);
+ }
+ request.writeInt(block.size());
+ for(Integer t: block) {
+ request.writeInt(t);
+ }
+ return native_setMetadataFilter(request);
+ }
+
+ /**
+ * Set the MediaPlayer2 to start when this MediaPlayer2 finishes playback
+ * (i.e. reaches the end of the stream).
+ * The media framework will attempt to transition from this player to
+ * the next as seamlessly as possible. The next player can be set at
+ * any time before completion, but shall be after setDataSource has been
+ * called successfully. The next player must be prepared by the
+ * app, and the application should not call play() on it.
+ * The next MediaPlayer2 must be different from 'this'. An exception
+ * will be thrown if next == this.
+ * The application may call setNextMediaPlayer(null) to indicate no
+ * next player should be started at the end of playback.
+ * If the current player is looping, it will keep looping and the next
+ * player will not be started.
+ *
+ * @param next the player to start after this one completes playback.
+ *
+ * @hide
+ */
+ @Override
+ public native void setNextMediaPlayer(MediaPlayer2 next);
+
+ /**
+ * Resets the MediaPlayer2 to its uninitialized state. After calling
+ * this method, you will have to initialize it again by setting the
+ * data source and calling prepare().
+ */
+ @Override
+ public void reset() {
+ mSelectedSubtitleTrackIndex = -1;
+ synchronized(mOpenSubtitleSources) {
+ for (final InputStream is: mOpenSubtitleSources) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+ }
+ mOpenSubtitleSources.clear();
+ }
+ if (mSubtitleController != null) {
+ mSubtitleController.reset();
+ }
+ if (mTimeProvider != null) {
+ mTimeProvider.close();
+ mTimeProvider = null;
+ }
+
+ stayAwake(false);
+ _reset();
+ // make sure none of the listeners get called anymore
+ if (mEventHandler != null) {
+ mEventHandler.removeCallbacksAndMessages(null);
+ }
+
+ synchronized (mIndexTrackPairs) {
+ mIndexTrackPairs.clear();
+ mInbandTrackIndices.clear();
+ };
+
+ resetDrmState();
+ }
+
+ private native void _reset();
+
+ /**
+ * Set up a timer for {@link #TimeProvider}. {@link #TimeProvider} will be
+ * notified when the presentation time reaches (becomes greater than or equal to)
+ * the value specified.
+ *
+ * @param mediaTimeUs presentation time to get timed event callback at
+ * @hide
+ */
+ @Override
+ public void notifyAt(long mediaTimeUs) {
+ _notifyAt(mediaTimeUs);
+ }
+
+ private native void _notifyAt(long mediaTimeUs);
+
+ // Keep KEY_PARAMETER_* in sync with include/media/mediaplayer2.h
+ private final static int KEY_PARAMETER_AUDIO_ATTRIBUTES = 1400;
+ /**
+ * Sets the parameter indicated by key.
+ * @param key key indicates the parameter to be set.
+ * @param value value of the parameter to be set.
+ * @return true if the parameter is set successfully, false otherwise
+ * {@hide}
+ */
+ private native boolean setParameter(int key, Parcel value);
+
+ /**
+ * Sets the audio attributes for this MediaPlayer2.
+ * See {@link AudioAttributes} for how to build and configure an instance of this class.
+ * You must call this method before {@link #prepare()} or {@link #prepareAsync()} in order
+ * for the audio attributes to become effective thereafter.
+ * @param attributes a non-null set of audio attributes
+ * @throws IllegalArgumentException if the attributes are null or invalid.
+ */
+ @Override
+ public void setAudioAttributes(AudioAttributes attributes) {
+ if (attributes == null) {
+ final String msg = "Cannot set AudioAttributes to null";
+ throw new IllegalArgumentException(msg);
+ }
+ mUsage = attributes.getUsage();
+ mBypassInterruptionPolicy = (attributes.getAllFlags()
+ & AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY) != 0;
+ Parcel pattributes = Parcel.obtain();
+ attributes.writeToParcel(pattributes, AudioAttributes.FLATTEN_TAGS);
+ setParameter(KEY_PARAMETER_AUDIO_ATTRIBUTES, pattributes);
+ pattributes.recycle();
+ }
+
+ /**
+ * Sets the player to be looping or non-looping.
+ *
+ * @param looping whether to loop or not
+ * @hide
+ */
+ @Override
+ public native void setLooping(boolean looping);
+
+ /**
+ * Checks whether the MediaPlayer2 is looping or non-looping.
+ *
+ * @return true if the MediaPlayer2 is currently looping, false otherwise
+ * @hide
+ */
+ @Override
+ public native boolean isLooping();
+
+ /**
+ * Sets the volume on this player.
+ * This API is recommended for balancing the output of audio streams
+ * within an application. Unless you are writing an application to
+ * control user settings, this API should be used in preference to
+ * {@link AudioManager#setStreamVolume(int, int, int)} which sets the volume of ALL streams of
+ * a particular type. Note that the passed volume values are raw scalars in range 0.0 to 1.0.
+ * UI controls should be scaled logarithmically.
+ *
+ * @param leftVolume left volume scalar
+ * @param rightVolume right volume scalar
+ */
+ /*
+ * FIXME: Merge this into javadoc comment above when setVolume(float) is not @hide.
+ * The single parameter form below is preferred if the channel volumes don't need
+ * to be set independently.
+ */
+ @Override
+ public void setVolume(float leftVolume, float rightVolume) {
+ _setVolume(leftVolume, rightVolume);
+ }
+
+ private native void _setVolume(float leftVolume, float rightVolume);
+
+ /**
+ * Similar, excepts sets volume of all channels to same value.
+ * @hide
+ */
+ @Override
+ public void setVolume(float volume) {
+ setVolume(volume, volume);
+ }
+
+ /**
+ * Sets the audio session ID.
+ *
+ * @param sessionId the audio session ID.
+ * The audio session ID is a system wide unique identifier for the audio stream played by
+ * this MediaPlayer2 instance.
+ * The primary use of the audio session ID is to associate audio effects to a particular
+ * instance of MediaPlayer2: if an audio session ID is provided when creating an audio effect,
+ * this effect will be applied only to the audio content of media players within the same
+ * audio session and not to the output mix.
+ * When created, a MediaPlayer2 instance automatically generates its own audio session ID.
+ * However, it is possible to force this player to be part of an already existing audio session
+ * by calling this method.
+ * This method must be called before one of the overloaded <code> setDataSource </code> methods.
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if the sessionId is invalid.
+ */
+ @Override
+ public native void setAudioSessionId(int sessionId);
+
+ /**
+ * Returns the audio session ID.
+ *
+ * @return the audio session ID. {@see #setAudioSessionId(int)}
+ * Note that the audio session ID is 0 only if a problem occured when the MediaPlayer2 was contructed.
+ */
+ @Override
+ public native int getAudioSessionId();
+
+ /**
+ * Attaches an auxiliary effect to the player. A typical auxiliary effect is a reverberation
+ * effect which can be applied on any sound source that directs a certain amount of its
+ * energy to this effect. This amount is defined by setAuxEffectSendLevel().
+ * See {@link #setAuxEffectSendLevel(float)}.
+ * <p>After creating an auxiliary effect (e.g.
+ * {@link android.media.audiofx.EnvironmentalReverb}), retrieve its ID with
+ * {@link android.media.audiofx.AudioEffect#getId()} and use it when calling this method
+ * to attach the player to the effect.
+ * <p>To detach the effect from the player, call this method with a null effect id.
+ * <p>This method must be called after one of the overloaded <code> setDataSource </code>
+ * methods.
+ * @param effectId system wide unique id of the effect to attach
+ */
+ @Override
+ public native void attachAuxEffect(int effectId);
+
+
+ /**
+ * Sets the send level of the player to the attached auxiliary effect.
+ * See {@link #attachAuxEffect(int)}. The level value range is 0 to 1.0.
+ * <p>By default the send level is 0, so even if an effect is attached to the player
+ * this method must be called for the effect to be applied.
+ * <p>Note that the passed level value is a raw scalar. UI controls should be scaled
+ * logarithmically: the gain applied by audio framework ranges from -72dB to 0dB,
+ * so an appropriate conversion from linear UI input x to level is:
+ * x == 0 -> level = 0
+ * 0 < x <= R -> level = 10^(72*(x-R)/20/R)
+ * @param level send level scalar
+ */
+ @Override
+ public void setAuxEffectSendLevel(float level) {
+ _setAuxEffectSendLevel(level);
+ }
+
+ private native void _setAuxEffectSendLevel(float level);
+
+ /*
+ * @param request Parcel destinated to the media player.
+ * @param reply[out] Parcel that will contain the reply.
+ * @return The status code.
+ */
+ private native final int native_invoke(Parcel request, Parcel reply);
+
+
+ /*
+ * @param update_only If true fetch only the set of metadata that have
+ * changed since the last invocation of getMetadata.
+ * The set is built using the unfiltered
+ * notifications the native player sent to the
+ * MediaPlayer2Manager during that period of
+ * time. If false, all the metadatas are considered.
+ * @param apply_filter If true, once the metadata set has been built based on
+ * the value update_only, the current filter is applied.
+ * @param reply[out] On return contains the serialized
+ * metadata. Valid only if the call was successful.
+ * @return The status code.
+ */
+ private native final boolean native_getMetadata(boolean update_only,
+ boolean apply_filter,
+ Parcel reply);
+
+ /*
+ * @param request Parcel with the 2 serialized lists of allowed
+ * metadata types followed by the one to be
+ * dropped. Each list starts with an integer
+ * indicating the number of metadata type elements.
+ * @return The status code.
+ */
+ private native final int native_setMetadataFilter(Parcel request);
+
+ private static native final void native_init();
+ private native final void native_setup(Object mediaplayer2_this);
+ private native final void native_finalize();
+
+ /**
+ * Class for MediaPlayer2 to return each audio/video/subtitle track's metadata.
+ *
+ * @see android.media.MediaPlayer2#getTrackInfo
+ */
+ public static final class TrackInfoImpl extends TrackInfo {
+ /**
+ * Gets the track type.
+ * @return TrackType which indicates if the track is video, audio, timed text.
+ */
+ @Override
+ public int getTrackType() {
+ return mTrackType;
+ }
+
+ /**
+ * Gets the language code of the track.
+ * @return a language code in either way of ISO-639-1 or ISO-639-2.
+ * When the language is unknown or could not be determined,
+ * ISO-639-2 language code, "und", is returned.
+ */
+ @Override
+ public String getLanguage() {
+ String language = mFormat.getString(MediaFormat.KEY_LANGUAGE);
+ return language == null ? "und" : language;
+ }
+
+ /**
+ * Gets the {@link MediaFormat} of the track. If the format is
+ * unknown or could not be determined, null is returned.
+ */
+ @Override
+ public MediaFormat getFormat() {
+ if (mTrackType == MEDIA_TRACK_TYPE_TIMEDTEXT
+ || mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
+ return mFormat;
+ }
+ return null;
+ }
+
+ final int mTrackType;
+ final MediaFormat mFormat;
+
+ TrackInfoImpl(Parcel in) {
+ mTrackType = in.readInt();
+ // TODO: parcel in the full MediaFormat; currently we are using createSubtitleFormat
+ // even for audio/video tracks, meaning we only set the mime and language.
+ String mime = in.readString();
+ String language = in.readString();
+ mFormat = MediaFormat.createSubtitleFormat(mime, language);
+
+ if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
+ mFormat.setInteger(MediaFormat.KEY_IS_AUTOSELECT, in.readInt());
+ mFormat.setInteger(MediaFormat.KEY_IS_DEFAULT, in.readInt());
+ mFormat.setInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, in.readInt());
+ }
+ }
+
+ /** @hide */
+ TrackInfoImpl(int type, MediaFormat format) {
+ mTrackType = type;
+ mFormat = format;
+ }
+
+ /**
+ * Flatten this object in to a Parcel.
+ *
+ * @param dest The Parcel in which the object should be written.
+ * @param flags Additional flags about how the object should be written.
+ * May be 0 or {@link android.os.Parcelable#PARCELABLE_WRITE_RETURN_VALUE}.
+ */
+ /* package private */ void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mTrackType);
+ dest.writeString(getLanguage());
+
+ if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
+ dest.writeString(mFormat.getString(MediaFormat.KEY_MIME));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_AUTOSELECT));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_DEFAULT));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE));
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder out = new StringBuilder(128);
+ out.append(getClass().getName());
+ out.append('{');
+ switch (mTrackType) {
+ case MEDIA_TRACK_TYPE_VIDEO:
+ out.append("VIDEO");
+ break;
+ case MEDIA_TRACK_TYPE_AUDIO:
+ out.append("AUDIO");
+ break;
+ case MEDIA_TRACK_TYPE_TIMEDTEXT:
+ out.append("TIMEDTEXT");
+ break;
+ case MEDIA_TRACK_TYPE_SUBTITLE:
+ out.append("SUBTITLE");
+ break;
+ default:
+ out.append("UNKNOWN");
+ break;
+ }
+ out.append(", " + mFormat.toString());
+ out.append("}");
+ return out.toString();
+ }
+
+ /**
+ * Used to read a TrackInfoImpl from a Parcel.
+ */
+ /* package private */ static final Parcelable.Creator<TrackInfoImpl> CREATOR
+ = new Parcelable.Creator<TrackInfoImpl>() {
+ @Override
+ public TrackInfoImpl createFromParcel(Parcel in) {
+ return new TrackInfoImpl(in);
+ }
+
+ @Override
+ public TrackInfoImpl[] newArray(int size) {
+ return new TrackInfoImpl[size];
+ }
+ };
+
+ };
+
+ // We would like domain specific classes with more informative names than the `first` and `second`
+ // in generic Pair, but we would also like to avoid creating new/trivial classes. As a compromise
+ // we document the meanings of `first` and `second` here:
+ //
+ // Pair.first - inband track index; non-null iff representing an inband track.
+ // Pair.second - a SubtitleTrack registered with mSubtitleController; non-null iff representing
+ // an inband subtitle track or any out-of-band track (subtitle or timedtext).
+ private Vector<Pair<Integer, SubtitleTrack>> mIndexTrackPairs = new Vector<>();
+ private BitSet mInbandTrackIndices = new BitSet();
+
+ /**
+ * Returns a List of track information.
+ *
+ * @return List of track info. The total number of tracks is the array length.
+ * Must be called again if an external timed text source has been added after
+ * addTimedTextSource method is called.
+ * @throws IllegalStateException if it is called in an invalid state.
+ */
+ @Override
+ public List<TrackInfo> getTrackInfo() {
+ TrackInfoImpl trackInfo[] = getInbandTrackInfoImpl();
+ // add out-of-band tracks
+ synchronized (mIndexTrackPairs) {
+ TrackInfoImpl allTrackInfo[] = new TrackInfoImpl[mIndexTrackPairs.size()];
+ for (int i = 0; i < allTrackInfo.length; i++) {
+ Pair<Integer, SubtitleTrack> p = mIndexTrackPairs.get(i);
+ if (p.first != null) {
+ // inband track
+ allTrackInfo[i] = trackInfo[p.first];
+ } else {
+ SubtitleTrack track = p.second;
+ allTrackInfo[i] = new TrackInfoImpl(track.getTrackType(), track.getFormat());
+ }
+ }
+ return Arrays.asList(allTrackInfo);
+ }
+ }
+
+ private TrackInfoImpl[] getInbandTrackInfoImpl() throws IllegalStateException {
+ Parcel request = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ try {
+ request.writeInt(INVOKE_ID_GET_TRACK_INFO);
+ invoke(request, reply);
+ TrackInfoImpl trackInfo[] = reply.createTypedArray(TrackInfoImpl.CREATOR);
+ return trackInfo;
+ } finally {
+ request.recycle();
+ reply.recycle();
+ }
+ }
+
+ /*
+ * A helper function to check if the mime type is supported by media framework.
+ */
+ private static boolean availableMimeTypeForExternalSource(String mimeType) {
+ if (MEDIA_MIMETYPE_TEXT_SUBRIP.equals(mimeType)) {
+ return true;
+ }
+ return false;
+ }
+
+ private SubtitleController mSubtitleController;
+
+ /** @hide */
+ @Override
+ public void setSubtitleAnchor(
+ SubtitleController controller,
+ SubtitleController.Anchor anchor) {
+ // TODO: create SubtitleController in MediaPlayer2
+ mSubtitleController = controller;
+ mSubtitleController.setAnchor(anchor);
+ }
+
+ /**
+ * The private version of setSubtitleAnchor is used internally to set mSubtitleController if
+ * necessary when clients don't provide their own SubtitleControllers using the public version
+ * {@link #setSubtitleAnchor(SubtitleController, Anchor)} (e.g. {@link VideoView} provides one).
+ */
+ private synchronized void setSubtitleAnchor() {
+ if ((mSubtitleController == null) && (ActivityThread.currentApplication() != null)) {
+ final HandlerThread thread = new HandlerThread("SetSubtitleAnchorThread");
+ thread.start();
+ Handler handler = new Handler(thread.getLooper());
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ Context context = ActivityThread.currentApplication();
+ mSubtitleController = new SubtitleController(context, mTimeProvider, MediaPlayer2Impl.this);
+ mSubtitleController.setAnchor(new Anchor() {
+ @Override
+ public void setSubtitleWidget(RenderingWidget subtitleWidget) {
+ }
+
+ @Override
+ public Looper getSubtitleLooper() {
+ return Looper.getMainLooper();
+ }
+ });
+ thread.getLooper().quitSafely();
+ }
+ });
+ try {
+ thread.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ Log.w(TAG, "failed to join SetSubtitleAnchorThread");
+ }
+ }
+ }
+
+ private int mSelectedSubtitleTrackIndex = -1;
+ private Vector<InputStream> mOpenSubtitleSources;
+
+ private OnSubtitleDataListener mSubtitleDataListener = new OnSubtitleDataListener() {
+ @Override
+ public void onSubtitleData(MediaPlayer2 mp, SubtitleData data) {
+ int index = data.getTrackIndex();
+ synchronized (mIndexTrackPairs) {
+ for (Pair<Integer, SubtitleTrack> p : mIndexTrackPairs) {
+ if (p.first != null && p.first == index && p.second != null) {
+ // inband subtitle track that owns data
+ SubtitleTrack track = p.second;
+ track.onData(data);
+ }
+ }
+ }
+ }
+ };
+
+ /** @hide */
+ @Override
+ public void onSubtitleTrackSelected(SubtitleTrack track) {
+ if (mSelectedSubtitleTrackIndex >= 0) {
+ try {
+ selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, false);
+ } catch (IllegalStateException e) {
+ }
+ mSelectedSubtitleTrackIndex = -1;
+ }
+ setOnSubtitleDataListener(null);
+ if (track == null) {
+ return;
+ }
+
+ synchronized (mIndexTrackPairs) {
+ for (Pair<Integer, SubtitleTrack> p : mIndexTrackPairs) {
+ if (p.first != null && p.second == track) {
+ // inband subtitle track that is selected
+ mSelectedSubtitleTrackIndex = p.first;
+ break;
+ }
+ }
+ }
+
+ if (mSelectedSubtitleTrackIndex >= 0) {
+ try {
+ selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, true);
+ } catch (IllegalStateException e) {
+ }
+ setOnSubtitleDataListener(mSubtitleDataListener);
+ }
+ // no need to select out-of-band tracks
+ }
+
+ /** @hide */
+ @Override
+ public void addSubtitleSource(InputStream is, MediaFormat format)
+ throws IllegalStateException
+ {
+ final InputStream fIs = is;
+ final MediaFormat fFormat = format;
+
+ if (is != null) {
+ // Ensure all input streams are closed. It is also a handy
+ // way to implement timeouts in the future.
+ synchronized(mOpenSubtitleSources) {
+ mOpenSubtitleSources.add(is);
+ }
+ } else {
+ Log.w(TAG, "addSubtitleSource called with null InputStream");
+ }
+
+ getMediaTimeProvider();
+
+ // process each subtitle in its own thread
+ final HandlerThread thread = new HandlerThread("SubtitleReadThread",
+ Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
+ thread.start();
+ Handler handler = new Handler(thread.getLooper());
+ handler.post(new Runnable() {
+ private int addTrack() {
+ if (fIs == null || mSubtitleController == null) {
+ return MEDIA_INFO_UNSUPPORTED_SUBTITLE;
+ }
+
+ SubtitleTrack track = mSubtitleController.addTrack(fFormat);
+ if (track == null) {
+ return MEDIA_INFO_UNSUPPORTED_SUBTITLE;
+ }
+
+ // TODO: do the conversion in the subtitle track
+ Scanner scanner = new Scanner(fIs, "UTF-8");
+ String contents = scanner.useDelimiter("\\A").next();
+ synchronized(mOpenSubtitleSources) {
+ mOpenSubtitleSources.remove(fIs);
+ }
+ scanner.close();
+ synchronized (mIndexTrackPairs) {
+ mIndexTrackPairs.add(Pair.<Integer, SubtitleTrack>create(null, track));
+ }
+ Handler h = mTimeProvider.mEventHandler;
+ int what = TimeProvider.NOTIFY;
+ int arg1 = TimeProvider.NOTIFY_TRACK_DATA;
+ Pair<SubtitleTrack, byte[]> trackData = Pair.create(track, contents.getBytes());
+ Message m = h.obtainMessage(what, arg1, 0, trackData);
+ h.sendMessage(m);
+ return MEDIA_INFO_EXTERNAL_METADATA_UPDATE;
+ }
+
+ public void run() {
+ int res = addTrack();
+ if (mEventHandler != null) {
+ Message m = mEventHandler.obtainMessage(MEDIA_INFO, res, 0, null);
+ mEventHandler.sendMessage(m);
+ }
+ thread.getLooper().quitSafely();
+ }
+ });
+ }
+
+ private void scanInternalSubtitleTracks() {
+ setSubtitleAnchor();
+
+ populateInbandTracks();
+
+ if (mSubtitleController != null) {
+ mSubtitleController.selectDefaultTrack();
+ }
+ }
+
+ private void populateInbandTracks() {
+ TrackInfoImpl[] tracks = getInbandTrackInfoImpl();
+ synchronized (mIndexTrackPairs) {
+ for (int i = 0; i < tracks.length; i++) {
+ if (mInbandTrackIndices.get(i)) {
+ continue;
+ } else {
+ mInbandTrackIndices.set(i);
+ }
+
+ // newly appeared inband track
+ if (tracks[i].getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
+ SubtitleTrack track = mSubtitleController.addTrack(
+ tracks[i].getFormat());
+ mIndexTrackPairs.add(Pair.create(i, track));
+ } else {
+ mIndexTrackPairs.add(Pair.<Integer, SubtitleTrack>create(i, null));
+ }
+ }
+ }
+ }
+
+ /* TODO: Limit the total number of external timed text source to a reasonable number.
+ */
+ /**
+ * Adds an external timed text source file.
+ *
+ * Currently supported format is SubRip with the file extension .srt, case insensitive.
+ * Note that a single external timed text source may contain multiple tracks in it.
+ * One can find the total number of available tracks using {@link #getTrackInfo()} to see what
+ * additional tracks become available after this method call.
+ *
+ * @param path The file path of external timed text source file.
+ * @param mimeType The mime type of the file. Must be one of the mime types listed above.
+ * @throws IOException if the file cannot be accessed or is corrupted.
+ * @throws IllegalArgumentException if the mimeType is not supported.
+ * @throws IllegalStateException if called in an invalid state.
+ * @hide
+ */
+ @Override
+ public void addTimedTextSource(String path, String mimeType)
+ throws IOException {
+ if (!availableMimeTypeForExternalSource(mimeType)) {
+ final String msg = "Illegal mimeType for timed text source: " + mimeType;
+ throw new IllegalArgumentException(msg);
+ }
+
+ File file = new File(path);
+ if (file.exists()) {
+ FileInputStream is = new FileInputStream(file);
+ FileDescriptor fd = is.getFD();
+ addTimedTextSource(fd, mimeType);
+ is.close();
+ } else {
+ // We do not support the case where the path is not a file.
+ throw new IOException(path);
+ }
+ }
+
+
+ /**
+ * Adds an external timed text source file (Uri).
+ *
+ * Currently supported format is SubRip with the file extension .srt, case insensitive.
+ * Note that a single external timed text source may contain multiple tracks in it.
+ * One can find the total number of available tracks using {@link #getTrackInfo()} to see what
+ * additional tracks become available after this method call.
+ *
+ * @param context the Context to use when resolving the Uri
+ * @param uri the Content URI of the data you want to play
+ * @param mimeType The mime type of the file. Must be one of the mime types listed above.
+ * @throws IOException if the file cannot be accessed or is corrupted.
+ * @throws IllegalArgumentException if the mimeType is not supported.
+ * @throws IllegalStateException if called in an invalid state.
+ * @hide
+ */
+ @Override
+ public void addTimedTextSource(Context context, Uri uri, String mimeType)
+ throws IOException {
+ String scheme = uri.getScheme();
+ if(scheme == null || scheme.equals("file")) {
+ addTimedTextSource(uri.getPath(), mimeType);
+ return;
+ }
+
+ AssetFileDescriptor fd = null;
+ try {
+ ContentResolver resolver = context.getContentResolver();
+ fd = resolver.openAssetFileDescriptor(uri, "r");
+ if (fd == null) {
+ return;
+ }
+ addTimedTextSource(fd.getFileDescriptor(), mimeType);
+ return;
+ } catch (SecurityException ex) {
+ } catch (IOException ex) {
+ } finally {
+ if (fd != null) {
+ fd.close();
+ }
+ }
+ }
+
+ /**
+ * Adds an external timed text source file (FileDescriptor).
+ *
+ * It is the caller's responsibility to close the file descriptor.
+ * It is safe to do so as soon as this call returns.
+ *
+ * Currently supported format is SubRip. Note that a single external timed text source may
+ * contain multiple tracks in it. One can find the total number of available tracks
+ * using {@link #getTrackInfo()} to see what additional tracks become available
+ * after this method call.
+ *
+ * @param fd the FileDescriptor for the file you want to play
+ * @param mimeType The mime type of the file. Must be one of the mime types listed above.
+ * @throws IllegalArgumentException if the mimeType is not supported.
+ * @throws IllegalStateException if called in an invalid state.
+ * @hide
+ */
+ @Override
+ public void addTimedTextSource(FileDescriptor fd, String mimeType) {
+ // intentionally less than LONG_MAX
+ addTimedTextSource(fd, 0, 0x7ffffffffffffffL, mimeType);
+ }
+
+ /**
+ * Adds an external timed text file (FileDescriptor).
+ *
+ * It is the caller's responsibility to close the file descriptor.
+ * It is safe to do so as soon as this call returns.
+ *
+ * Currently supported format is SubRip. Note that a single external timed text source may
+ * contain multiple tracks in it. One can find the total number of available tracks
+ * using {@link #getTrackInfo()} to see what additional tracks become available
+ * after this method call.
+ *
+ * @param fd the FileDescriptor for the file you want to play
+ * @param offset the offset into the file where the data to be played starts, in bytes
+ * @param length the length in bytes of the data to be played
+ * @param mime The mime type of the file. Must be one of the mime types listed above.
+ * @throws IllegalArgumentException if the mimeType is not supported.
+ * @throws IllegalStateException if called in an invalid state.
+ * @hide
+ */
+ @Override
+ public void addTimedTextSource(FileDescriptor fd, long offset, long length, String mime) {
+ if (!availableMimeTypeForExternalSource(mime)) {
+ throw new IllegalArgumentException("Illegal mimeType for timed text source: " + mime);
+ }
+
+ final FileDescriptor dupedFd;
+ try {
+ dupedFd = Os.dup(fd);
+ } catch (ErrnoException ex) {
+ Log.e(TAG, ex.getMessage(), ex);
+ throw new RuntimeException(ex);
+ }
+
+ final MediaFormat fFormat = new MediaFormat();
+ fFormat.setString(MediaFormat.KEY_MIME, mime);
+ fFormat.setInteger(MediaFormat.KEY_IS_TIMED_TEXT, 1);
+
+ // A MediaPlayer2 created by a VideoView should already have its mSubtitleController set.
+ if (mSubtitleController == null) {
+ setSubtitleAnchor();
+ }
+
+ if (!mSubtitleController.hasRendererFor(fFormat)) {
+ // test and add not atomic
+ Context context = ActivityThread.currentApplication();
+ mSubtitleController.registerRenderer(new SRTRenderer(context, mEventHandler));
+ }
+ final SubtitleTrack track = mSubtitleController.addTrack(fFormat);
+ synchronized (mIndexTrackPairs) {
+ mIndexTrackPairs.add(Pair.<Integer, SubtitleTrack>create(null, track));
+ }
+
+ getMediaTimeProvider();
+
+ final long offset2 = offset;
+ final long length2 = length;
+ final HandlerThread thread = new HandlerThread(
+ "TimedTextReadThread",
+ Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
+ thread.start();
+ Handler handler = new Handler(thread.getLooper());
+ handler.post(new Runnable() {
+ private int addTrack() {
+ final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try {
+ Os.lseek(dupedFd, offset2, OsConstants.SEEK_SET);
+ byte[] buffer = new byte[4096];
+ for (long total = 0; total < length2;) {
+ int bytesToRead = (int) Math.min(buffer.length, length2 - total);
+ int bytes = IoBridge.read(dupedFd, buffer, 0, bytesToRead);
+ if (bytes < 0) {
+ break;
+ } else {
+ bos.write(buffer, 0, bytes);
+ total += bytes;
+ }
+ }
+ Handler h = mTimeProvider.mEventHandler;
+ int what = TimeProvider.NOTIFY;
+ int arg1 = TimeProvider.NOTIFY_TRACK_DATA;
+ Pair<SubtitleTrack, byte[]> trackData = Pair.create(track, bos.toByteArray());
+ Message m = h.obtainMessage(what, arg1, 0, trackData);
+ h.sendMessage(m);
+ return MEDIA_INFO_EXTERNAL_METADATA_UPDATE;
+ } catch (Exception e) {
+ Log.e(TAG, e.getMessage(), e);
+ return MEDIA_INFO_TIMED_TEXT_ERROR;
+ } finally {
+ try {
+ Os.close(dupedFd);
+ } catch (ErrnoException e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ }
+ }
+
+ public void run() {
+ int res = addTrack();
+ if (mEventHandler != null) {
+ Message m = mEventHandler.obtainMessage(MEDIA_INFO, res, 0, null);
+ mEventHandler.sendMessage(m);
+ }
+ thread.getLooper().quitSafely();
+ }
+ });
+ }
+
+ /**
+ * Returns the index of the audio, video, or subtitle track currently selected for playback,
+ * The return value is an index into the array returned by {@link #getTrackInfo()}, and can
+ * be used in calls to {@link #selectTrack(int)} or {@link #deselectTrack(int)}.
+ *
+ * @param trackType should be one of {@link TrackInfo#MEDIA_TRACK_TYPE_VIDEO},
+ * {@link TrackInfo#MEDIA_TRACK_TYPE_AUDIO}, or
+ * {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE}
+ * @return index of the audio, video, or subtitle track currently selected for playback;
+ * a negative integer is returned when there is no selected track for {@code trackType} or
+ * when {@code trackType} is not one of audio, video, or subtitle.
+ * @throws IllegalStateException if called after {@link #close()}
+ *
+ * @see #getTrackInfo()
+ * @see #selectTrack(int)
+ * @see #deselectTrack(int)
+ */
+ @Override
+ public int getSelectedTrack(int trackType) {
+ if (mSubtitleController != null
+ && (trackType == TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE
+ || trackType == TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT)) {
+ SubtitleTrack subtitleTrack = mSubtitleController.getSelectedTrack();
+ if (subtitleTrack != null) {
+ synchronized (mIndexTrackPairs) {
+ for (int i = 0; i < mIndexTrackPairs.size(); i++) {
+ Pair<Integer, SubtitleTrack> p = mIndexTrackPairs.get(i);
+ if (p.second == subtitleTrack && subtitleTrack.getTrackType() == trackType) {
+ return i;
+ }
+ }
+ }
+ }
+ }
+
+ Parcel request = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ try {
+ request.writeInt(INVOKE_ID_GET_SELECTED_TRACK);
+ request.writeInt(trackType);
+ invoke(request, reply);
+ int inbandTrackIndex = reply.readInt();
+ synchronized (mIndexTrackPairs) {
+ for (int i = 0; i < mIndexTrackPairs.size(); i++) {
+ Pair<Integer, SubtitleTrack> p = mIndexTrackPairs.get(i);
+ if (p.first != null && p.first == inbandTrackIndex) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ } finally {
+ request.recycle();
+ reply.recycle();
+ }
+ }
+
+ /**
+ * Selects a track.
+ * <p>
+ * If a MediaPlayer2 is in invalid state, it throws an IllegalStateException exception.
+ * If a MediaPlayer2 is in <em>Started</em> state, the selected track is presented immediately.
+ * If a MediaPlayer2 is not in Started state, it just marks the track to be played.
+ * </p>
+ * <p>
+ * In any valid state, if it is called multiple times on the same type of track (ie. Video,
+ * Audio, Timed Text), the most recent one will be chosen.
+ * </p>
+ * <p>
+ * The first audio and video tracks are selected by default if available, even though
+ * this method is not called. However, no timed text track will be selected until
+ * this function is called.
+ * </p>
+ * <p>
+ * Currently, only timed text tracks or audio tracks can be selected via this method.
+ * In addition, the support for selecting an audio track at runtime is pretty limited
+ * in that an audio track can only be selected in the <em>Prepared</em> state.
+ * </p>
+ * @param index the index of the track to be selected. The valid range of the index
+ * is 0..total number of track - 1. The total number of tracks as well as the type of
+ * each individual track can be found by calling {@link #getTrackInfo()} method.
+ * @throws IllegalStateException if called in an invalid state.
+ *
+ * @see android.media.MediaPlayer2#getTrackInfo
+ */
+ @Override
+ public void selectTrack(int index) {
+ selectOrDeselectTrack(index, true /* select */);
+ }
+
+ /**
+ * Deselect a track.
+ * <p>
+ * Currently, the track must be a timed text track and no audio or video tracks can be
+ * deselected. If the timed text track identified by index has not been
+ * selected before, it throws an exception.
+ * </p>
+ * @param index the index of the track to be deselected. The valid range of the index
+ * is 0..total number of tracks - 1. The total number of tracks as well as the type of
+ * each individual track can be found by calling {@link #getTrackInfo()} method.
+ * @throws IllegalStateException if called in an invalid state.
+ *
+ * @see android.media.MediaPlayer2#getTrackInfo
+ */
+ @Override
+ public void deselectTrack(int index) {
+ selectOrDeselectTrack(index, false /* select */);
+ }
+
+ private void selectOrDeselectTrack(int index, boolean select)
+ throws IllegalStateException {
+ // handle subtitle track through subtitle controller
+ populateInbandTracks();
+
+ Pair<Integer,SubtitleTrack> p = null;
+ try {
+ p = mIndexTrackPairs.get(index);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // ignore bad index
+ return;
+ }
+
+ SubtitleTrack track = p.second;
+ if (track == null) {
+ // inband (de)select
+ selectOrDeselectInbandTrack(p.first, select);
+ return;
+ }
+
+ if (mSubtitleController == null) {
+ return;
+ }
+
+ if (!select) {
+ // out-of-band deselect
+ if (mSubtitleController.getSelectedTrack() == track) {
+ mSubtitleController.selectTrack(null);
+ } else {
+ Log.w(TAG, "trying to deselect track that was not selected");
+ }
+ return;
+ }
+
+ // out-of-band select
+ if (track.getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT) {
+ int ttIndex = getSelectedTrack(TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT);
+ synchronized (mIndexTrackPairs) {
+ if (ttIndex >= 0 && ttIndex < mIndexTrackPairs.size()) {
+ Pair<Integer,SubtitleTrack> p2 = mIndexTrackPairs.get(ttIndex);
+ if (p2.first != null && p2.second == null) {
+ // deselect inband counterpart
+ selectOrDeselectInbandTrack(p2.first, false);
+ }
+ }
+ }
+ }
+ mSubtitleController.selectTrack(track);
+ }
+
+ private void selectOrDeselectInbandTrack(int index, boolean select)
+ throws IllegalStateException {
+ Parcel request = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ try {
+ request.writeInt(select? INVOKE_ID_SELECT_TRACK: INVOKE_ID_DESELECT_TRACK);
+ request.writeInt(index);
+ invoke(request, reply);
+ } finally {
+ request.recycle();
+ reply.recycle();
+ }
+ }
+
+ /**
+ * Sets the target UDP re-transmit endpoint for the low level player.
+ * Generally, the address portion of the endpoint is an IP multicast
+ * address, although a unicast address would be equally valid. When a valid
+ * retransmit endpoint has been set, the media player will not decode and
+ * render the media presentation locally. Instead, the player will attempt
+ * to re-multiplex its media data using the Android@Home RTP profile and
+ * re-transmit to the target endpoint. Receiver devices (which may be
+ * either the same as the transmitting device or different devices) may
+ * instantiate, prepare, and start a receiver player using a setDataSource
+ * URL of the form...
+ *
+ * aahRX://<multicastIP>:<port>
+ *
+ * to receive, decode and render the re-transmitted content.
+ *
+ * setRetransmitEndpoint may only be called before setDataSource has been
+ * called; while the player is in the Idle state.
+ *
+ * @param endpoint the address and UDP port of the re-transmission target or
+ * null if no re-transmission is to be performed.
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if the retransmit endpoint is supplied,
+ * but invalid.
+ *
+ * {@hide} pending API council
+ */
+ @Override
+ public void setRetransmitEndpoint(InetSocketAddress endpoint)
+ throws IllegalStateException, IllegalArgumentException
+ {
+ String addrString = null;
+ int port = 0;
+
+ if (null != endpoint) {
+ addrString = endpoint.getAddress().getHostAddress();
+ port = endpoint.getPort();
+ }
+
+ int ret = native_setRetransmitEndpoint(addrString, port);
+ if (ret != 0) {
+ throw new IllegalArgumentException("Illegal re-transmit endpoint; native ret " + ret);
+ }
+ }
+
+ private native final int native_setRetransmitEndpoint(String addrString, int port);
+
+ /**
+ * Releases the resources held by this {@code MediaPlayer2} object.
+ *
+ * It is considered good practice to call this method when you're
+ * done using the MediaPlayer2. In particular, whenever an Activity
+ * of an application is paused (its onPause() method is called),
+ * or stopped (its onStop() method is called), this method should be
+ * invoked to release the MediaPlayer2 object, unless the application
+ * has a special need to keep the object around. In addition to
+ * unnecessary resources (such as memory and instances of codecs)
+ * being held, failure to call this method immediately if a
+ * MediaPlayer2 object is no longer needed may also lead to
+ * continuous battery consumption for mobile devices, and playback
+ * failure for other applications if no multiple instances of the
+ * same codec are supported on a device. Even if multiple instances
+ * of the same codec are supported, some performance degradation
+ * may be expected when unnecessary multiple instances are used
+ * at the same time.
+ *
+ * {@code close()} may be safely called after a prior {@code close()}.
+ * This class implements the Java {@code AutoCloseable} interface and
+ * may be used with try-with-resources.
+ */
+ @Override
+ public void close() {
+ synchronized (mGuard) {
+ release();
+ }
+ }
+
+ // Have to declare protected for finalize() since it is protected
+ // in the base class Object.
+ @Override
+ protected void finalize() throws Throwable {
+ if (mGuard != null) {
+ mGuard.warnIfOpen();
+ }
+
+ close();
+ native_finalize();
+ }
+
+ private void release() {
+ stayAwake(false);
+ updateSurfaceScreenOn();
+ synchronized (mEventCbLock) {
+ mEventCb = null;
+ mEventExec = null;
+ }
+ if (mTimeProvider != null) {
+ mTimeProvider.close();
+ mTimeProvider = null;
+ }
+ mOnSubtitleDataListener = null;
+
+ // Modular DRM clean up
+ mOnDrmConfigHelper = null;
+ synchronized (mDrmEventCbLock) {
+ mDrmEventCb = null;
+ mDrmEventExec = null;
+ }
+ resetDrmState();
+
+ _release();
+ }
+
+ private native void _release();
+
+ /* Do not change these values without updating their counterparts
+ * in include/media/mediaplayer2.h!
+ */
+ private static final int MEDIA_NOP = 0; // interface test message
+ private static final int MEDIA_PREPARED = 1;
+ private static final int MEDIA_PLAYBACK_COMPLETE = 2;
+ private static final int MEDIA_BUFFERING_UPDATE = 3;
+ private static final int MEDIA_SEEK_COMPLETE = 4;
+ private static final int MEDIA_SET_VIDEO_SIZE = 5;
+ private static final int MEDIA_STARTED = 6;
+ private static final int MEDIA_PAUSED = 7;
+ private static final int MEDIA_STOPPED = 8;
+ private static final int MEDIA_SKIPPED = 9;
+ private static final int MEDIA_NOTIFY_TIME = 98;
+ private static final int MEDIA_TIMED_TEXT = 99;
+ private static final int MEDIA_ERROR = 100;
+ private static final int MEDIA_INFO = 200;
+ private static final int MEDIA_SUBTITLE_DATA = 201;
+ private static final int MEDIA_META_DATA = 202;
+ private static final int MEDIA_DRM_INFO = 210;
+ private static final int MEDIA_AUDIO_ROUTING_CHANGED = 10000;
+
+ private TimeProvider mTimeProvider;
+
+ /** @hide */
+ @Override
+ public MediaTimeProvider getMediaTimeProvider() {
+ if (mTimeProvider == null) {
+ mTimeProvider = new TimeProvider(this);
+ }
+ return mTimeProvider;
+ }
+
+ private class EventHandler extends Handler {
+ private MediaPlayer2Impl mMediaPlayer;
+
+ public EventHandler(MediaPlayer2Impl mp, Looper looper) {
+ super(looper);
+ mMediaPlayer = mp;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (mMediaPlayer.mNativeContext == 0) {
+ Log.w(TAG, "mediaplayer2 went away with unhandled events");
+ return;
+ }
+ final Executor eventExec;
+ final EventCallback eventCb;
+ synchronized (mEventCbLock) {
+ eventExec = mEventExec;
+ eventCb = mEventCb;
+ }
+ final Executor drmEventExec;
+ final DrmEventCallback drmEventCb;
+ synchronized (mDrmEventCbLock) {
+ drmEventExec = mDrmEventExec;
+ drmEventCb = mDrmEventCb;
+ }
+ switch(msg.what) {
+ case MEDIA_PREPARED:
+ try {
+ scanInternalSubtitleTracks();
+ } catch (RuntimeException e) {
+ // send error message instead of crashing;
+ // send error message instead of inlining a call to onError
+ // to avoid code duplication.
+ Message msg2 = obtainMessage(
+ MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED, null);
+ sendMessage(msg2);
+ }
+
+ if (eventCb != null && eventExec != null) {
+ eventExec.execute(() -> eventCb.onInfo(
+ mMediaPlayer, 0, MEDIA_INFO_PREPARED, 0));
+ }
+ return;
+
+ case MEDIA_DRM_INFO:
+ Log.v(TAG, "MEDIA_DRM_INFO " + mDrmEventCb);
+
+ if (msg.obj == null) {
+ Log.w(TAG, "MEDIA_DRM_INFO msg.obj=NULL");
+ } else if (msg.obj instanceof Parcel) {
+ if (drmEventExec != null && drmEventCb != null) {
+ // The parcel was parsed already in postEventFromNative
+ final DrmInfoImpl drmInfo;
+
+ synchronized (mDrmLock) {
+ if (mDrmInfoImpl != null) {
+ drmInfo = mDrmInfoImpl.makeCopy();
+ } else {
+ drmInfo = null;
+ }
+ }
+
+ // notifying the client outside the lock
+ if (drmInfo != null) {
+ drmEventExec.execute(() -> drmEventCb.onDrmInfo(mMediaPlayer, drmInfo));
+ }
+ }
+ } else {
+ Log.w(TAG, "MEDIA_DRM_INFO msg.obj of unexpected type " + msg.obj);
+ }
+ return;
+
+ case MEDIA_PLAYBACK_COMPLETE:
+ if (eventCb != null && eventExec != null) {
+ eventExec.execute(() -> eventCb.onInfo(
+ mMediaPlayer, 0, MEDIA_INFO_PLAYBACK_COMPLETE, 0));
+ }
+ stayAwake(false);
+ return;
+
+ case MEDIA_STOPPED:
+ {
+ TimeProvider timeProvider = mTimeProvider;
+ if (timeProvider != null) {
+ timeProvider.onStopped();
+ }
+ }
+ break;
+
+ case MEDIA_STARTED:
+ case MEDIA_PAUSED:
+ {
+ TimeProvider timeProvider = mTimeProvider;
+ if (timeProvider != null) {
+ timeProvider.onPaused(msg.what == MEDIA_PAUSED);
+ }
+ }
+ break;
+
+ case MEDIA_BUFFERING_UPDATE:
+ if (eventCb != null && eventExec != null) {
+ final int percent = msg.arg1;
+ eventExec.execute(() -> eventCb.onBufferingUpdate(mMediaPlayer, 0, percent));
+ }
+ return;
+
+ case MEDIA_SEEK_COMPLETE:
+ if (eventCb != null && eventExec != null) {
+ eventExec.execute(() -> eventCb.onInfo(
+ mMediaPlayer, 0, MEDIA_INFO_COMPLETE_CALL_SEEK, 0));
+ }
+ // fall through
+
+ case MEDIA_SKIPPED:
+ {
+ TimeProvider timeProvider = mTimeProvider;
+ if (timeProvider != null) {
+ timeProvider.onSeekComplete(mMediaPlayer);
+ }
+ }
+ return;
+
+ case MEDIA_SET_VIDEO_SIZE:
+ if (eventCb != null && eventExec != null) {
+ final int width = msg.arg1;
+ final int height = msg.arg2;
+ eventExec.execute(() -> eventCb.onVideoSizeChanged(
+ mMediaPlayer, 0, width, height));
+ }
+ return;
+
+ case MEDIA_ERROR:
+ Log.e(TAG, "Error (" + msg.arg1 + "," + msg.arg2 + ")");
+ if (eventCb != null && eventExec != null) {
+ final int what = msg.arg1;
+ final int extra = msg.arg2;
+ eventExec.execute(() -> eventCb.onError(mMediaPlayer, 0, what, extra));
+ eventExec.execute(() -> eventCb.onInfo(
+ mMediaPlayer, 0, MEDIA_INFO_PLAYBACK_COMPLETE, 0));
+ }
+ stayAwake(false);
+ return;
+
+ case MEDIA_INFO:
+ switch (msg.arg1) {
+ case MEDIA_INFO_VIDEO_TRACK_LAGGING:
+ Log.i(TAG, "Info (" + msg.arg1 + "," + msg.arg2 + ")");
+ break;
+ case MEDIA_INFO_METADATA_UPDATE:
+ try {
+ scanInternalSubtitleTracks();
+ } catch (RuntimeException e) {
+ Message msg2 = obtainMessage(
+ MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED, null);
+ sendMessage(msg2);
+ }
+ // fall through
+
+ case MEDIA_INFO_EXTERNAL_METADATA_UPDATE:
+ msg.arg1 = MEDIA_INFO_METADATA_UPDATE;
+ // update default track selection
+ if (mSubtitleController != null) {
+ mSubtitleController.selectDefaultTrack();
+ }
+ break;
+ case MEDIA_INFO_BUFFERING_START:
+ case MEDIA_INFO_BUFFERING_END:
+ TimeProvider timeProvider = mTimeProvider;
+ if (timeProvider != null) {
+ timeProvider.onBuffering(msg.arg1 == MEDIA_INFO_BUFFERING_START);
+ }
+ break;
+ }
+
+ if (eventCb != null && eventExec != null) {
+ final int what = msg.arg1;
+ final int extra = msg.arg2;
+ eventExec.execute(() -> eventCb.onInfo(mMediaPlayer, 0, what, extra));
+ }
+ // No real default action so far.
+ return;
+
+ case MEDIA_NOTIFY_TIME:
+ TimeProvider timeProvider = mTimeProvider;
+ if (timeProvider != null) {
+ timeProvider.onNotifyTime();
+ }
+ return;
+
+ case MEDIA_TIMED_TEXT:
+ if (eventCb == null || eventExec == null) {
+ return;
+ }
+ if (msg.obj == null) {
+ eventExec.execute(() -> eventCb.onTimedText(mMediaPlayer, 0, null));
+ } else {
+ if (msg.obj instanceof Parcel) {
+ Parcel parcel = (Parcel)msg.obj;
+ TimedText text = new TimedText(parcel);
+ parcel.recycle();
+ eventExec.execute(() -> eventCb.onTimedText(mMediaPlayer, 0, text));
+ }
+ }
+ return;
+
+ case MEDIA_SUBTITLE_DATA:
+ OnSubtitleDataListener onSubtitleDataListener = mOnSubtitleDataListener;
+ if (onSubtitleDataListener == null) {
+ return;
+ }
+ if (msg.obj instanceof Parcel) {
+ Parcel parcel = (Parcel) msg.obj;
+ SubtitleData data = new SubtitleData(parcel);
+ parcel.recycle();
+ onSubtitleDataListener.onSubtitleData(mMediaPlayer, data);
+ }
+ return;
+
+ case MEDIA_META_DATA:
+ if (eventCb == null || eventExec == null) {
+ return;
+ }
+ if (msg.obj instanceof Parcel) {
+ Parcel parcel = (Parcel) msg.obj;
+ TimedMetaData data = TimedMetaData.createTimedMetaDataFromParcel(parcel);
+ parcel.recycle();
+ eventExec.execute(() -> eventCb.onTimedMetaDataAvailable(
+ mMediaPlayer, 0, data));
+ }
+ return;
+
+ case MEDIA_NOP: // interface test message - ignore
+ break;
+
+ case MEDIA_AUDIO_ROUTING_CHANGED:
+ AudioManager.resetAudioPortGeneration();
+ synchronized (mRoutingChangeListeners) {
+ for (NativeRoutingEventHandlerDelegate delegate
+ : mRoutingChangeListeners.values()) {
+ delegate.notifyClient();
+ }
+ }
+ return;
+
+ default:
+ Log.e(TAG, "Unknown message type " + msg.what);
+ return;
+ }
+ }
+ }
+
+ /*
+ * Called from native code when an interesting event happens. This method
+ * just uses the EventHandler system to post the event back to the main app thread.
+ * We use a weak reference to the original MediaPlayer2 object so that the native
+ * code is safe from the object disappearing from underneath it. (This is
+ * the cookie passed to native_setup().)
+ */
+ private static void postEventFromNative(Object mediaplayer2_ref,
+ int what, int arg1, int arg2, Object obj)
+ {
+ final MediaPlayer2Impl mp = (MediaPlayer2Impl)((WeakReference)mediaplayer2_ref).get();
+ if (mp == null) {
+ return;
+ }
+
+ switch (what) {
+ case MEDIA_INFO:
+ if (arg1 == MEDIA_INFO_STARTED_AS_NEXT) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ // this acquires the wakelock if needed, and sets the client side state
+ mp.play();
+ }
+ }).start();
+ Thread.yield();
+ }
+ break;
+
+ case MEDIA_DRM_INFO:
+ // We need to derive mDrmInfoImpl before prepare() returns so processing it here
+ // before the notification is sent to EventHandler below. EventHandler runs in the
+ // notification looper so its handleMessage might process the event after prepare()
+ // has returned.
+ Log.v(TAG, "postEventFromNative MEDIA_DRM_INFO");
+ if (obj instanceof Parcel) {
+ Parcel parcel = (Parcel)obj;
+ DrmInfoImpl drmInfo = new DrmInfoImpl(parcel);
+ synchronized (mp.mDrmLock) {
+ mp.mDrmInfoImpl = drmInfo;
+ }
+ } else {
+ Log.w(TAG, "MEDIA_DRM_INFO msg.obj of unexpected type " + obj);
+ }
+ break;
+
+ case MEDIA_PREPARED:
+ // By this time, we've learned about DrmInfo's presence or absence. This is meant
+ // mainly for prepareAsync() use case. For prepare(), this still can run to a race
+ // condition b/c MediaPlayerNative releases the prepare() lock before calling notify
+ // so we also set mDrmInfoResolved in prepare().
+ synchronized (mp.mDrmLock) {
+ mp.mDrmInfoResolved = true;
+ }
+ break;
+
+ }
+
+ if (mp.mEventHandler != null) {
+ Message m = mp.mEventHandler.obtainMessage(what, arg1, arg2, obj);
+ mp.mEventHandler.sendMessage(m);
+ }
+ }
+
+ private Executor mEventExec;
+ private EventCallback mEventCb;
+ private final Object mEventCbLock = new Object();
+
+ /**
+ * Register a callback to be invoked when the media source is ready
+ * for playback.
+ *
+ * @param eventCallback the callback that will be run
+ * @param executor the executor through which the callback should be invoked
+ */
+ @Override
+ public void registerEventCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull EventCallback eventCallback) {
+ if (eventCallback == null) {
+ throw new IllegalArgumentException("Illegal null EventCallback");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Illegal null Executor for the EventCallback");
+ }
+ synchronized (mEventCbLock) {
+ // TODO: support multiple callbacks.
+ mEventExec = executor;
+ mEventCb = eventCallback;
+ }
+ }
+
+ /**
+ * Unregisters an {@link EventCallback}.
+ *
+ * @param callback an {@link EventCallback} to unregister
+ */
+ @Override
+ public void unregisterEventCallback(EventCallback callback) {
+ synchronized (mEventCbLock) {
+ if (callback == mEventCb) {
+ mEventExec = null;
+ mEventCb = null;
+ }
+ }
+ }
+
+ /**
+ * Register a callback to be invoked when a track has data available.
+ *
+ * @param listener the callback that will be run
+ *
+ * @hide
+ */
+ @Override
+ public void setOnSubtitleDataListener(OnSubtitleDataListener listener) {
+ mOnSubtitleDataListener = listener;
+ }
+
+ private OnSubtitleDataListener mOnSubtitleDataListener;
+
+
+ // Modular DRM begin
+
+ /**
+ * Register a callback to be invoked for configuration of the DRM object before
+ * the session is created.
+ * The callback will be invoked synchronously during the execution
+ * of {@link #prepareDrm(UUID uuid)}.
+ *
+ * @param listener the callback that will be run
+ */
+ @Override
+ public void setOnDrmConfigHelper(OnDrmConfigHelper listener)
+ {
+ synchronized (mDrmLock) {
+ mOnDrmConfigHelper = listener;
+ } // synchronized
+ }
+
+ private OnDrmConfigHelper mOnDrmConfigHelper;
+
+ private Executor mDrmEventExec;
+ private DrmEventCallback mDrmEventCb;
+ private final Object mDrmEventCbLock = new Object();
+
+ /**
+ * Register a callback to be invoked when the media source is ready
+ * for playback.
+ *
+ * @param eventCallback the callback that will be run
+ * @param executor the executor through which the callback should be invoked
+ */
+ @Override
+ public void registerDrmEventCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull DrmEventCallback eventCallback) {
+ if (eventCallback == null) {
+ throw new IllegalArgumentException("Illegal null EventCallback");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Illegal null Executor for the EventCallback");
+ }
+ synchronized (mDrmEventCbLock) {
+ // TODO: support multiple callbacks.
+ mDrmEventExec = executor;
+ mDrmEventCb = eventCallback;
+ }
+ }
+
+ /**
+ * Unregisters a {@link DrmEventCallback}.
+ *
+ * @param callback a {@link DrmEventCallback} to unregister
+ */
+ @Override
+ public void unregisterDrmEventCallback(DrmEventCallback callback) {
+ synchronized (mDrmEventCbLock) {
+ if (callback == mDrmEventCb) {
+ mDrmEventExec = null;
+ mDrmEventCb = null;
+ }
+ }
+ }
+
+
+ /**
+ * Retrieves the DRM Info associated with the current source
+ *
+ * @throws IllegalStateException if called before prepare()
+ */
+ @Override
+ public DrmInfo getDrmInfo() {
+ DrmInfoImpl drmInfo = null;
+
+ // there is not much point if the app calls getDrmInfo within an OnDrmInfoListenet;
+ // regardless below returns drmInfo anyway instead of raising an exception
+ synchronized (mDrmLock) {
+ if (!mDrmInfoResolved && mDrmInfoImpl == null) {
+ final String msg = "The Player has not been prepared yet";
+ Log.v(TAG, msg);
+ throw new IllegalStateException(msg);
+ }
+
+ if (mDrmInfoImpl != null) {
+ drmInfo = mDrmInfoImpl.makeCopy();
+ }
+ } // synchronized
+
+ return drmInfo;
+ }
+
+
+ /**
+ * Prepares the DRM for the current source
+ * <p>
+ * If {@code OnDrmConfigHelper} is registered, it will be called during
+ * preparation to allow configuration of the DRM properties before opening the
+ * DRM session. Note that the callback is called synchronously in the thread that called
+ * {@code prepareDrm}. It should be used only for a series of {@code getDrmPropertyString}
+ * and {@code setDrmPropertyString} calls and refrain from any lengthy operation.
+ * <p>
+ * If the device has not been provisioned before, this call also provisions the device
+ * which involves accessing the provisioning server and can take a variable time to
+ * complete depending on the network connectivity.
+ * If {@code OnDrmPreparedListener} is registered, prepareDrm() runs in non-blocking
+ * mode by launching the provisioning in the background and returning. The listener
+ * will be called when provisioning and preparation has finished. If a
+ * {@code OnDrmPreparedListener} is not registered, prepareDrm() waits till provisioning
+ * and preparation has finished, i.e., runs in blocking mode.
+ * <p>
+ * If {@code OnDrmPreparedListener} is registered, it is called to indicate the DRM
+ * session being ready. The application should not make any assumption about its call
+ * sequence (e.g., before or after prepareDrm returns), or the thread context that will
+ * execute the listener (unless the listener is registered with a handler thread).
+ * <p>
+ *
+ * @param uuid The UUID of the crypto scheme. If not known beforehand, it can be retrieved
+ * from the source through {@code getDrmInfo} or registering a {@code onDrmInfoListener}.
+ *
+ * @throws IllegalStateException if called before prepare(), or the DRM was
+ * prepared already
+ * @throws UnsupportedSchemeException if the crypto scheme is not supported
+ * @throws ResourceBusyException if required DRM resources are in use
+ * @throws ProvisioningNetworkErrorException if provisioning is required but failed due to a
+ * network error
+ * @throws ProvisioningServerErrorException if provisioning is required but failed due to
+ * the request denied by the provisioning server
+ */
+ @Override
+ public void prepareDrm(@NonNull UUID uuid)
+ throws UnsupportedSchemeException, ResourceBusyException,
+ ProvisioningNetworkErrorException, ProvisioningServerErrorException
+ {
+ Log.v(TAG, "prepareDrm: uuid: " + uuid + " mOnDrmConfigHelper: " + mOnDrmConfigHelper);
+
+ boolean allDoneWithoutProvisioning = false;
+
+ synchronized (mDrmLock) {
+
+ // only allowing if tied to a protected source; might relax for releasing offline keys
+ if (mDrmInfoImpl == null) {
+ final String msg = "prepareDrm(): Wrong usage: The player must be prepared and " +
+ "DRM info be retrieved before this call.";
+ Log.e(TAG, msg);
+ throw new IllegalStateException(msg);
+ }
+
+ if (mActiveDrmScheme) {
+ final String msg = "prepareDrm(): Wrong usage: There is already " +
+ "an active DRM scheme with " + mDrmUUID;
+ Log.e(TAG, msg);
+ throw new IllegalStateException(msg);
+ }
+
+ if (mPrepareDrmInProgress) {
+ final String msg = "prepareDrm(): Wrong usage: There is already " +
+ "a pending prepareDrm call.";
+ Log.e(TAG, msg);
+ throw new IllegalStateException(msg);
+ }
+
+ if (mDrmProvisioningInProgress) {
+ final String msg = "prepareDrm(): Unexpectd: Provisioning is already in progress.";
+ Log.e(TAG, msg);
+ throw new IllegalStateException(msg);
+ }
+
+ // shouldn't need this; just for safeguard
+ cleanDrmObj();
+
+ mPrepareDrmInProgress = true;
+
+ try {
+ // only creating the DRM object to allow pre-openSession configuration
+ prepareDrm_createDrmStep(uuid);
+ } catch (Exception e) {
+ Log.w(TAG, "prepareDrm(): Exception ", e);
+ mPrepareDrmInProgress = false;
+ throw e;
+ }
+
+ mDrmConfigAllowed = true;
+ } // synchronized
+
+
+ // call the callback outside the lock
+ if (mOnDrmConfigHelper != null) {
+ mOnDrmConfigHelper.onDrmConfig(this);
+ }
+
+ synchronized (mDrmLock) {
+ mDrmConfigAllowed = false;
+ boolean earlyExit = false;
+
+ try {
+ prepareDrm_openSessionStep(uuid);
+
+ mDrmUUID = uuid;
+ mActiveDrmScheme = true;
+
+ allDoneWithoutProvisioning = true;
+ } catch (IllegalStateException e) {
+ final String msg = "prepareDrm(): Wrong usage: The player must be " +
+ "in the prepared state to call prepareDrm().";
+ Log.e(TAG, msg);
+ earlyExit = true;
+ throw new IllegalStateException(msg);
+ } catch (NotProvisionedException e) {
+ Log.w(TAG, "prepareDrm: NotProvisionedException");
+
+ // handle provisioning internally; it'll reset mPrepareDrmInProgress
+ int result = HandleProvisioninig(uuid);
+
+ // if blocking mode, we're already done;
+ // if non-blocking mode, we attempted to launch background provisioning
+ if (result != PREPARE_DRM_STATUS_SUCCESS) {
+ earlyExit = true;
+ String msg;
+
+ switch (result) {
+ case PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR:
+ msg = "prepareDrm: Provisioning was required but failed " +
+ "due to a network error.";
+ Log.e(TAG, msg);
+ throw new ProvisioningNetworkErrorExceptionImpl(msg);
+
+ case PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR:
+ msg = "prepareDrm: Provisioning was required but the request " +
+ "was denied by the server.";
+ Log.e(TAG, msg);
+ throw new ProvisioningServerErrorExceptionImpl(msg);
+
+ case PREPARE_DRM_STATUS_PREPARATION_ERROR:
+ default: // default for safeguard
+ msg = "prepareDrm: Post-provisioning preparation failed.";
+ Log.e(TAG, msg);
+ throw new IllegalStateException(msg);
+ }
+ }
+ // nothing else to do;
+ // if blocking or non-blocking, HandleProvisioninig does the re-attempt & cleanup
+ } catch (Exception e) {
+ Log.e(TAG, "prepareDrm: Exception " + e);
+ earlyExit = true;
+ throw e;
+ } finally {
+ if (!mDrmProvisioningInProgress) {// if early exit other than provisioning exception
+ mPrepareDrmInProgress = false;
+ }
+ if (earlyExit) { // cleaning up object if didn't succeed
+ cleanDrmObj();
+ }
+ } // finally
+ } // synchronized
+
+
+ // if finished successfully without provisioning, call the callback outside the lock
+ if (allDoneWithoutProvisioning) {
+ final Executor drmEventExec;
+ final DrmEventCallback drmEventCb;
+ synchronized (mDrmEventCbLock) {
+ drmEventExec = mDrmEventExec;
+ drmEventCb = mDrmEventCb;
+ }
+ if (drmEventExec != null && drmEventCb != null) {
+ drmEventExec.execute(() -> drmEventCb.onDrmPrepared(
+ this, PREPARE_DRM_STATUS_SUCCESS));
+ }
+ }
+
+ }
+
+
+ private native void _releaseDrm();
+
+ /**
+ * Releases the DRM session
+ * <p>
+ * The player has to have an active DRM session and be in stopped, or prepared
+ * state before this call is made.
+ * A {@code reset()} call will release the DRM session implicitly.
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session to release
+ */
+ @Override
+ public void releaseDrm()
+ throws NoDrmSchemeException
+ {
+ Log.v(TAG, "releaseDrm:");
+
+ synchronized (mDrmLock) {
+ if (!mActiveDrmScheme) {
+ Log.e(TAG, "releaseDrm(): No active DRM scheme to release.");
+ throw new NoDrmSchemeExceptionImpl("releaseDrm: No active DRM scheme to release.");
+ }
+
+ try {
+ // we don't have the player's state in this layer. The below call raises
+ // exception if we're in a non-stopped/prepared state.
+
+ // for cleaning native/mediaserver crypto object
+ _releaseDrm();
+
+ // for cleaning client-side MediaDrm object; only called if above has succeeded
+ cleanDrmObj();
+
+ mActiveDrmScheme = false;
+ } catch (IllegalStateException e) {
+ Log.w(TAG, "releaseDrm: Exception ", e);
+ throw new IllegalStateException("releaseDrm: The player is not in a valid state.");
+ } catch (Exception e) {
+ Log.e(TAG, "releaseDrm: Exception ", e);
+ }
+ } // synchronized
+ }
+
+
+ /**
+ * A key request/response exchange occurs between the app and a license server
+ * to obtain or release keys used to decrypt encrypted content.
+ * <p>
+ * getKeyRequest() is used to obtain an opaque key request byte array that is
+ * delivered to the license server. The opaque key request byte array is returned
+ * in KeyRequest.data. The recommended URL to deliver the key request to is
+ * returned in KeyRequest.defaultUrl.
+ * <p>
+ * After the app has received the key request response from the server,
+ * it should deliver to the response to the DRM engine plugin using the method
+ * {@link #provideKeyResponse}.
+ *
+ * @param keySetId is the key-set identifier of the offline keys being released when keyType is
+ * {@link MediaDrm#KEY_TYPE_RELEASE}. It should be set to null for other key requests, when
+ * keyType is {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}.
+ *
+ * @param initData is the container-specific initialization data when the keyType is
+ * {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}. Its meaning is
+ * interpreted based on the mime type provided in the mimeType parameter. It could
+ * contain, for example, the content ID, key ID or other data obtained from the content
+ * metadata that is required in generating the key request.
+ * When the keyType is {@link MediaDrm#KEY_TYPE_RELEASE}, it should be set to null.
+ *
+ * @param mimeType identifies the mime type of the content
+ *
+ * @param keyType specifies the type of the request. The request may be to acquire
+ * keys for streaming, {@link MediaDrm#KEY_TYPE_STREAMING}, or for offline content
+ * {@link MediaDrm#KEY_TYPE_OFFLINE}, or to release previously acquired
+ * keys ({@link MediaDrm#KEY_TYPE_RELEASE}), which are identified by a keySetId.
+ *
+ * @param optionalParameters are included in the key request message to
+ * allow a client application to provide additional message parameters to the server.
+ * This may be {@code null} if no additional parameters are to be sent.
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session
+ */
+ @Override
+ @NonNull
+ public MediaDrm.KeyRequest getKeyRequest(@Nullable byte[] keySetId, @Nullable byte[] initData,
+ @Nullable String mimeType, @MediaDrm.KeyType int keyType,
+ @Nullable Map<String, String> optionalParameters)
+ throws NoDrmSchemeException
+ {
+ Log.v(TAG, "getKeyRequest: " +
+ " keySetId: " + keySetId + " initData:" + initData + " mimeType: " + mimeType +
+ " keyType: " + keyType + " optionalParameters: " + optionalParameters);
+
+ synchronized (mDrmLock) {
+ if (!mActiveDrmScheme) {
+ Log.e(TAG, "getKeyRequest NoDrmSchemeException");
+ throw new NoDrmSchemeExceptionImpl("getKeyRequest: Has to set a DRM scheme first.");
+ }
+
+ try {
+ byte[] scope = (keyType != MediaDrm.KEY_TYPE_RELEASE) ?
+ mDrmSessionId : // sessionId for KEY_TYPE_STREAMING/OFFLINE
+ keySetId; // keySetId for KEY_TYPE_RELEASE
+
+ HashMap<String, String> hmapOptionalParameters =
+ (optionalParameters != null) ?
+ new HashMap<String, String>(optionalParameters) :
+ null;
+
+ MediaDrm.KeyRequest request = mDrmObj.getKeyRequest(scope, initData, mimeType,
+ keyType, hmapOptionalParameters);
+ Log.v(TAG, "getKeyRequest: --> request: " + request);
+
+ return request;
+
+ } catch (NotProvisionedException e) {
+ Log.w(TAG, "getKeyRequest NotProvisionedException: " +
+ "Unexpected. Shouldn't have reached here.");
+ throw new IllegalStateException("getKeyRequest: Unexpected provisioning error.");
+ } catch (Exception e) {
+ Log.w(TAG, "getKeyRequest Exception " + e);
+ throw e;
+ }
+
+ } // synchronized
+ }
+
+
+ /**
+ * A key response is received from the license server by the app, then it is
+ * provided to the DRM engine plugin using provideKeyResponse. When the
+ * response is for an offline key request, a key-set identifier is returned that
+ * can be used to later restore the keys to a new session with the method
+ * {@ link # restoreKeys}.
+ * When the response is for a streaming or release request, null is returned.
+ *
+ * @param keySetId When the response is for a release request, keySetId identifies
+ * the saved key associated with the release request (i.e., the same keySetId
+ * passed to the earlier {@ link # getKeyRequest} call. It MUST be null when the
+ * response is for either streaming or offline key requests.
+ *
+ * @param response the byte array response from the server
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session
+ * @throws DeniedByServerException if the response indicates that the
+ * server rejected the request
+ */
+ @Override
+ public byte[] provideKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response)
+ throws NoDrmSchemeException, DeniedByServerException
+ {
+ Log.v(TAG, "provideKeyResponse: keySetId: " + keySetId + " response: " + response);
+
+ synchronized (mDrmLock) {
+
+ if (!mActiveDrmScheme) {
+ Log.e(TAG, "getKeyRequest NoDrmSchemeException");
+ throw new NoDrmSchemeExceptionImpl("getKeyRequest: Has to set a DRM scheme first.");
+ }
+
+ try {
+ byte[] scope = (keySetId == null) ?
+ mDrmSessionId : // sessionId for KEY_TYPE_STREAMING/OFFLINE
+ keySetId; // keySetId for KEY_TYPE_RELEASE
+
+ byte[] keySetResult = mDrmObj.provideKeyResponse(scope, response);
+
+ Log.v(TAG, "provideKeyResponse: keySetId: " + keySetId + " response: " + response +
+ " --> " + keySetResult);
+
+
+ return keySetResult;
+
+ } catch (NotProvisionedException e) {
+ Log.w(TAG, "provideKeyResponse NotProvisionedException: " +
+ "Unexpected. Shouldn't have reached here.");
+ throw new IllegalStateException("provideKeyResponse: " +
+ "Unexpected provisioning error.");
+ } catch (Exception e) {
+ Log.w(TAG, "provideKeyResponse Exception " + e);
+ throw e;
+ }
+ } // synchronized
+ }
+
+
+ /**
+ * Restore persisted offline keys into a new session. keySetId identifies the
+ * keys to load, obtained from a prior call to {@link #provideKeyResponse}.
+ *
+ * @param keySetId identifies the saved key set to restore
+ */
+ @Override
+ public void restoreKeys(@NonNull byte[] keySetId)
+ throws NoDrmSchemeException
+ {
+ Log.v(TAG, "restoreKeys: keySetId: " + keySetId);
+
+ synchronized (mDrmLock) {
+
+ if (!mActiveDrmScheme) {
+ Log.w(TAG, "restoreKeys NoDrmSchemeException");
+ throw new NoDrmSchemeExceptionImpl("restoreKeys: Has to set a DRM scheme first.");
+ }
+
+ try {
+ mDrmObj.restoreKeys(mDrmSessionId, keySetId);
+ } catch (Exception e) {
+ Log.w(TAG, "restoreKeys Exception " + e);
+ throw e;
+ }
+
+ } // synchronized
+ }
+
+
+ /**
+ * Read a DRM engine plugin String property value, given the property name string.
+ * <p>
+ * @param propertyName the property name
+ *
+ * Standard fields names are:
+ * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+ * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+ */
+ @Override
+ @NonNull
+ public String getDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName)
+ throws NoDrmSchemeException
+ {
+ Log.v(TAG, "getDrmPropertyString: propertyName: " + propertyName);
+
+ String value;
+ synchronized (mDrmLock) {
+
+ if (!mActiveDrmScheme && !mDrmConfigAllowed) {
+ Log.w(TAG, "getDrmPropertyString NoDrmSchemeException");
+ throw new NoDrmSchemeExceptionImpl("getDrmPropertyString: Has to prepareDrm() first.");
+ }
+
+ try {
+ value = mDrmObj.getPropertyString(propertyName);
+ } catch (Exception e) {
+ Log.w(TAG, "getDrmPropertyString Exception " + e);
+ throw e;
+ }
+ } // synchronized
+
+ Log.v(TAG, "getDrmPropertyString: propertyName: " + propertyName + " --> value: " + value);
+
+ return value;
+ }
+
+
+ /**
+ * Set a DRM engine plugin String property value.
+ * <p>
+ * @param propertyName the property name
+ * @param value the property value
+ *
+ * Standard fields names are:
+ * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+ * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+ */
+ @Override
+ public void setDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName,
+ @NonNull String value)
+ throws NoDrmSchemeException
+ {
+ Log.v(TAG, "setDrmPropertyString: propertyName: " + propertyName + " value: " + value);
+
+ synchronized (mDrmLock) {
+
+ if ( !mActiveDrmScheme && !mDrmConfigAllowed ) {
+ Log.w(TAG, "setDrmPropertyString NoDrmSchemeException");
+ throw new NoDrmSchemeExceptionImpl("setDrmPropertyString: Has to prepareDrm() first.");
+ }
+
+ try {
+ mDrmObj.setPropertyString(propertyName, value);
+ } catch ( Exception e ) {
+ Log.w(TAG, "setDrmPropertyString Exception " + e);
+ throw e;
+ }
+ } // synchronized
+ }
+
+ /**
+ * Encapsulates the DRM properties of the source.
+ */
+ public static final class DrmInfoImpl extends DrmInfo {
+ private Map<UUID, byte[]> mapPssh;
+ private UUID[] supportedSchemes;
+
+ /**
+ * Returns the PSSH info of the data source for each supported DRM scheme.
+ */
+ @Override
+ public Map<UUID, byte[]> getPssh() {
+ return mapPssh;
+ }
+
+ /**
+ * Returns the intersection of the data source and the device DRM schemes.
+ * It effectively identifies the subset of the source's DRM schemes which
+ * are supported by the device too.
+ */
+ @Override
+ public List<UUID> getSupportedSchemes() {
+ return Arrays.asList(supportedSchemes);
+ }
+
+ private DrmInfoImpl(Map<UUID, byte[]> Pssh, UUID[] SupportedSchemes) {
+ mapPssh = Pssh;
+ supportedSchemes = SupportedSchemes;
+ }
+
+ private DrmInfoImpl(Parcel parcel) {
+ Log.v(TAG, "DrmInfoImpl(" + parcel + ") size " + parcel.dataSize());
+
+ int psshsize = parcel.readInt();
+ byte[] pssh = new byte[psshsize];
+ parcel.readByteArray(pssh);
+
+ Log.v(TAG, "DrmInfoImpl() PSSH: " + arrToHex(pssh));
+ mapPssh = parsePSSH(pssh, psshsize);
+ Log.v(TAG, "DrmInfoImpl() PSSH: " + mapPssh);
+
+ int supportedDRMsCount = parcel.readInt();
+ supportedSchemes = new UUID[supportedDRMsCount];
+ for (int i = 0; i < supportedDRMsCount; i++) {
+ byte[] uuid = new byte[16];
+ parcel.readByteArray(uuid);
+
+ supportedSchemes[i] = bytesToUUID(uuid);
+
+ Log.v(TAG, "DrmInfoImpl() supportedScheme[" + i + "]: " +
+ supportedSchemes[i]);
+ }
+
+ Log.v(TAG, "DrmInfoImpl() Parcel psshsize: " + psshsize +
+ " supportedDRMsCount: " + supportedDRMsCount);
+ }
+
+ private DrmInfoImpl makeCopy() {
+ return new DrmInfoImpl(this.mapPssh, this.supportedSchemes);
+ }
+
+ private String arrToHex(byte[] bytes) {
+ String out = "0x";
+ for (int i = 0; i < bytes.length; i++) {
+ out += String.format("%02x", bytes[i]);
+ }
+
+ return out;
+ }
+
+ private UUID bytesToUUID(byte[] uuid) {
+ long msb = 0, lsb = 0;
+ for (int i = 0; i < 8; i++) {
+ msb |= ( ((long)uuid[i] & 0xff) << (8 * (7 - i)) );
+ lsb |= ( ((long)uuid[i+8] & 0xff) << (8 * (7 - i)) );
+ }
+
+ return new UUID(msb, lsb);
+ }
+
+ private Map<UUID, byte[]> parsePSSH(byte[] pssh, int psshsize) {
+ Map<UUID, byte[]> result = new HashMap<UUID, byte[]>();
+
+ final int UUID_SIZE = 16;
+ final int DATALEN_SIZE = 4;
+
+ int len = psshsize;
+ int numentries = 0;
+ int i = 0;
+
+ while (len > 0) {
+ if (len < UUID_SIZE) {
+ Log.w(TAG, String.format("parsePSSH: len is too short to parse " +
+ "UUID: (%d < 16) pssh: %d", len, psshsize));
+ return null;
+ }
+
+ byte[] subset = Arrays.copyOfRange(pssh, i, i + UUID_SIZE);
+ UUID uuid = bytesToUUID(subset);
+ i += UUID_SIZE;
+ len -= UUID_SIZE;
+
+ // get data length
+ if (len < 4) {
+ Log.w(TAG, String.format("parsePSSH: len is too short to parse " +
+ "datalen: (%d < 4) pssh: %d", len, psshsize));
+ return null;
+ }
+
+ subset = Arrays.copyOfRange(pssh, i, i+DATALEN_SIZE);
+ int datalen = (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) ?
+ ((subset[3] & 0xff) << 24) | ((subset[2] & 0xff) << 16) |
+ ((subset[1] & 0xff) << 8) | (subset[0] & 0xff) :
+ ((subset[0] & 0xff) << 24) | ((subset[1] & 0xff) << 16) |
+ ((subset[2] & 0xff) << 8) | (subset[3] & 0xff) ;
+ i += DATALEN_SIZE;
+ len -= DATALEN_SIZE;
+
+ if (len < datalen) {
+ Log.w(TAG, String.format("parsePSSH: len is too short to parse " +
+ "data: (%d < %d) pssh: %d", len, datalen, psshsize));
+ return null;
+ }
+
+ byte[] data = Arrays.copyOfRange(pssh, i, i+datalen);
+
+ // skip the data
+ i += datalen;
+ len -= datalen;
+
+ Log.v(TAG, String.format("parsePSSH[%d]: <%s, %s> pssh: %d",
+ numentries, uuid, arrToHex(data), psshsize));
+ numentries++;
+ result.put(uuid, data);
+ }
+
+ return result;
+ }
+
+ }; // DrmInfoImpl
+
+ /**
+ * Thrown when a DRM method is called before preparing a DRM scheme through prepareDrm().
+ * Extends MediaDrm.MediaDrmException
+ */
+ public static final class NoDrmSchemeExceptionImpl extends NoDrmSchemeException {
+ public NoDrmSchemeExceptionImpl(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ /**
+ * Thrown when the device requires DRM provisioning but the provisioning attempt has
+ * failed due to a network error (Internet reachability, timeout, etc.).
+ * Extends MediaDrm.MediaDrmException
+ */
+ public static final class ProvisioningNetworkErrorExceptionImpl
+ extends ProvisioningNetworkErrorException {
+ public ProvisioningNetworkErrorExceptionImpl(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ /**
+ * Thrown when the device requires DRM provisioning but the provisioning attempt has
+ * failed due to the provisioning server denying the request.
+ * Extends MediaDrm.MediaDrmException
+ */
+ public static final class ProvisioningServerErrorExceptionImpl
+ extends ProvisioningServerErrorException {
+ public ProvisioningServerErrorExceptionImpl(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+
+ private native void _prepareDrm(@NonNull byte[] uuid, @NonNull byte[] drmSessionId);
+
+ // Modular DRM helpers
+
+ private void prepareDrm_createDrmStep(@NonNull UUID uuid)
+ throws UnsupportedSchemeException {
+ Log.v(TAG, "prepareDrm_createDrmStep: UUID: " + uuid);
+
+ try {
+ mDrmObj = new MediaDrm(uuid);
+ Log.v(TAG, "prepareDrm_createDrmStep: Created mDrmObj=" + mDrmObj);
+ } catch (Exception e) { // UnsupportedSchemeException
+ Log.e(TAG, "prepareDrm_createDrmStep: MediaDrm failed with " + e);
+ throw e;
+ }
+ }
+
+ private void prepareDrm_openSessionStep(@NonNull UUID uuid)
+ throws NotProvisionedException, ResourceBusyException {
+ Log.v(TAG, "prepareDrm_openSessionStep: uuid: " + uuid);
+
+ // TODO: don't need an open session for a future specialKeyReleaseDrm mode but we should do
+ // it anyway so it raises provisioning error if needed. We'd rather handle provisioning
+ // at prepareDrm/openSession rather than getKeyRequest/provideKeyResponse
+ try {
+ mDrmSessionId = mDrmObj.openSession();
+ Log.v(TAG, "prepareDrm_openSessionStep: mDrmSessionId=" + mDrmSessionId);
+
+ // Sending it down to native/mediaserver to create the crypto object
+ // This call could simply fail due to bad player state, e.g., after play().
+ _prepareDrm(getByteArrayFromUUID(uuid), mDrmSessionId);
+ Log.v(TAG, "prepareDrm_openSessionStep: _prepareDrm/Crypto succeeded");
+
+ } catch (Exception e) { //ResourceBusyException, NotProvisionedException
+ Log.e(TAG, "prepareDrm_openSessionStep: open/crypto failed with " + e);
+ throw e;
+ }
+
+ }
+
+ private class ProvisioningThread extends Thread {
+ public static final int TIMEOUT_MS = 60000;
+
+ private UUID uuid;
+ private String urlStr;
+ private Object drmLock;
+ private MediaPlayer2Impl mediaPlayer;
+ private int status;
+ private boolean finished;
+ public int status() {
+ return status;
+ }
+
+ public ProvisioningThread initialize(MediaDrm.ProvisionRequest request,
+ UUID uuid, MediaPlayer2Impl mediaPlayer) {
+ // lock is held by the caller
+ drmLock = mediaPlayer.mDrmLock;
+ this.mediaPlayer = mediaPlayer;
+
+ urlStr = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
+ this.uuid = uuid;
+
+ status = PREPARE_DRM_STATUS_PREPARATION_ERROR;
+
+ Log.v(TAG, "HandleProvisioninig: Thread is initialised url: " + urlStr);
+ return this;
+ }
+
+ public void run() {
+
+ byte[] response = null;
+ boolean provisioningSucceeded = false;
+ try {
+ URL url = new URL(urlStr);
+ final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ try {
+ connection.setRequestMethod("POST");
+ connection.setDoOutput(false);
+ connection.setDoInput(true);
+ connection.setConnectTimeout(TIMEOUT_MS);
+ connection.setReadTimeout(TIMEOUT_MS);
+
+ connection.connect();
+ response = Streams.readFully(connection.getInputStream());
+
+ Log.v(TAG, "HandleProvisioninig: Thread run: response " +
+ response.length + " " + response);
+ } catch (Exception e) {
+ status = PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR;
+ Log.w(TAG, "HandleProvisioninig: Thread run: connect " + e + " url: " + url);
+ } finally {
+ connection.disconnect();
+ }
+ } catch (Exception e) {
+ status = PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR;
+ Log.w(TAG, "HandleProvisioninig: Thread run: openConnection " + e);
+ }
+
+ if (response != null) {
+ try {
+ mDrmObj.provideProvisionResponse(response);
+ Log.v(TAG, "HandleProvisioninig: Thread run: " +
+ "provideProvisionResponse SUCCEEDED!");
+
+ provisioningSucceeded = true;
+ } catch (Exception e) {
+ status = PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR;
+ Log.w(TAG, "HandleProvisioninig: Thread run: " +
+ "provideProvisionResponse " + e);
+ }
+ }
+
+ boolean succeeded = false;
+
+ final Executor drmEventExec;
+ final DrmEventCallback drmEventCb;
+ synchronized (mDrmEventCbLock) {
+ drmEventExec = mDrmEventExec;
+ drmEventCb = mDrmEventCb;
+ }
+ // non-blocking mode needs the lock
+ if (drmEventExec != null && drmEventCb != null) {
+
+ synchronized (drmLock) {
+ // continuing with prepareDrm
+ if (provisioningSucceeded) {
+ succeeded = mediaPlayer.resumePrepareDrm(uuid);
+ status = (succeeded) ?
+ PREPARE_DRM_STATUS_SUCCESS :
+ PREPARE_DRM_STATUS_PREPARATION_ERROR;
+ }
+ mediaPlayer.mDrmProvisioningInProgress = false;
+ mediaPlayer.mPrepareDrmInProgress = false;
+ if (!succeeded) {
+ cleanDrmObj(); // cleaning up if it hasn't gone through while in the lock
+ }
+ } // synchronized
+
+ // calling the callback outside the lock
+ drmEventExec.execute(() -> drmEventCb.onDrmPrepared(mediaPlayer, status));
+ } else { // blocking mode already has the lock
+
+ // continuing with prepareDrm
+ if (provisioningSucceeded) {
+ succeeded = mediaPlayer.resumePrepareDrm(uuid);
+ status = (succeeded) ?
+ PREPARE_DRM_STATUS_SUCCESS :
+ PREPARE_DRM_STATUS_PREPARATION_ERROR;
+ }
+ mediaPlayer.mDrmProvisioningInProgress = false;
+ mediaPlayer.mPrepareDrmInProgress = false;
+ if (!succeeded) {
+ cleanDrmObj(); // cleaning up if it hasn't gone through
+ }
+ }
+
+ finished = true;
+ } // run()
+
+ } // ProvisioningThread
+
+ private int HandleProvisioninig(UUID uuid) {
+ // the lock is already held by the caller
+
+ if (mDrmProvisioningInProgress) {
+ Log.e(TAG, "HandleProvisioninig: Unexpected mDrmProvisioningInProgress");
+ return PREPARE_DRM_STATUS_PREPARATION_ERROR;
+ }
+
+ MediaDrm.ProvisionRequest provReq = mDrmObj.getProvisionRequest();
+ if (provReq == null) {
+ Log.e(TAG, "HandleProvisioninig: getProvisionRequest returned null.");
+ return PREPARE_DRM_STATUS_PREPARATION_ERROR;
+ }
+
+ Log.v(TAG, "HandleProvisioninig provReq " +
+ " data: " + provReq.getData() + " url: " + provReq.getDefaultUrl());
+
+ // networking in a background thread
+ mDrmProvisioningInProgress = true;
+
+ mDrmProvisioningThread = new ProvisioningThread().initialize(provReq, uuid, this);
+ mDrmProvisioningThread.start();
+
+ int result;
+
+ // non-blocking: this is not the final result
+ final Executor drmEventExec;
+ final DrmEventCallback drmEventCb;
+ synchronized (mDrmEventCbLock) {
+ drmEventExec = mDrmEventExec;
+ drmEventCb = mDrmEventCb;
+ }
+ if (drmEventCb != null && drmEventExec != null) {
+ result = PREPARE_DRM_STATUS_SUCCESS;
+ } else {
+ // if blocking mode, wait till provisioning is done
+ try {
+ mDrmProvisioningThread.join();
+ } catch (Exception e) {
+ Log.w(TAG, "HandleProvisioninig: Thread.join Exception " + e);
+ }
+ result = mDrmProvisioningThread.status();
+ // no longer need the thread
+ mDrmProvisioningThread = null;
+ }
+
+ return result;
+ }
+
+ private boolean resumePrepareDrm(UUID uuid) {
+ Log.v(TAG, "resumePrepareDrm: uuid: " + uuid);
+
+ // mDrmLock is guaranteed to be held
+ boolean success = false;
+ try {
+ // resuming
+ prepareDrm_openSessionStep(uuid);
+
+ mDrmUUID = uuid;
+ mActiveDrmScheme = true;
+
+ success = true;
+ } catch (Exception e) {
+ Log.w(TAG, "HandleProvisioninig: Thread run _prepareDrm resume failed with " + e);
+ // mDrmObj clean up is done by the caller
+ }
+
+ return success;
+ }
+
+ private void resetDrmState() {
+ synchronized (mDrmLock) {
+ Log.v(TAG, "resetDrmState: " +
+ " mDrmInfoImpl=" + mDrmInfoImpl +
+ " mDrmProvisioningThread=" + mDrmProvisioningThread +
+ " mPrepareDrmInProgress=" + mPrepareDrmInProgress +
+ " mActiveDrmScheme=" + mActiveDrmScheme);
+
+ mDrmInfoResolved = false;
+ mDrmInfoImpl = null;
+
+ if (mDrmProvisioningThread != null) {
+ // timeout; relying on HttpUrlConnection
+ try {
+ mDrmProvisioningThread.join();
+ }
+ catch (InterruptedException e) {
+ Log.w(TAG, "resetDrmState: ProvThread.join Exception " + e);
+ }
+ mDrmProvisioningThread = null;
+ }
+
+ mPrepareDrmInProgress = false;
+ mActiveDrmScheme = false;
+
+ cleanDrmObj();
+ } // synchronized
+ }
+
+ private void cleanDrmObj() {
+ // the caller holds mDrmLock
+ Log.v(TAG, "cleanDrmObj: mDrmObj=" + mDrmObj + " mDrmSessionId=" + mDrmSessionId);
+
+ if (mDrmSessionId != null) {
+ mDrmObj.closeSession(mDrmSessionId);
+ mDrmSessionId = null;
+ }
+ if (mDrmObj != null) {
+ mDrmObj.release();
+ mDrmObj = null;
+ }
+ }
+
+ private static final byte[] getByteArrayFromUUID(@NonNull UUID uuid) {
+ long msb = uuid.getMostSignificantBits();
+ long lsb = uuid.getLeastSignificantBits();
+
+ byte[] uuidBytes = new byte[16];
+ for (int i = 0; i < 8; ++i) {
+ uuidBytes[i] = (byte)(msb >>> (8 * (7 - i)));
+ uuidBytes[8 + i] = (byte)(lsb >>> (8 * (7 - i)));
+ }
+
+ return uuidBytes;
+ }
+
+ // Modular DRM end
+
+ /*
+ * Test whether a given video scaling mode is supported.
+ */
+ private boolean isVideoScalingModeSupported(int mode) {
+ return (mode == VIDEO_SCALING_MODE_SCALE_TO_FIT ||
+ mode == VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING);
+ }
+
+ /** @hide */
+ static class TimeProvider implements MediaTimeProvider {
+ private static final String TAG = "MTP";
+ private static final long MAX_NS_WITHOUT_POSITION_CHECK = 5000000000L;
+ private static final long MAX_EARLY_CALLBACK_US = 1000;
+ private static final long TIME_ADJUSTMENT_RATE = 2; /* meaning 1/2 */
+ private long mLastTimeUs = 0;
+ private MediaPlayer2Impl mPlayer;
+ private boolean mPaused = true;
+ private boolean mStopped = true;
+ private boolean mBuffering;
+ private long mLastReportedTime;
+ // since we are expecting only a handful listeners per stream, there is
+ // no need for log(N) search performance
+ private MediaTimeProvider.OnMediaTimeListener mListeners[];
+ private long mTimes[];
+ private Handler mEventHandler;
+ private boolean mRefresh = false;
+ private boolean mPausing = false;
+ private boolean mSeeking = false;
+ private static final int NOTIFY = 1;
+ private static final int NOTIFY_TIME = 0;
+ private static final int NOTIFY_STOP = 2;
+ private static final int NOTIFY_SEEK = 3;
+ private static final int NOTIFY_TRACK_DATA = 4;
+ private HandlerThread mHandlerThread;
+
+ /** @hide */
+ public boolean DEBUG = false;
+
+ public TimeProvider(MediaPlayer2Impl mp) {
+ mPlayer = mp;
+ try {
+ getCurrentTimeUs(true, false);
+ } catch (IllegalStateException e) {
+ // we assume starting position
+ mRefresh = true;
+ }
+
+ Looper looper;
+ if ((looper = Looper.myLooper()) == null &&
+ (looper = Looper.getMainLooper()) == null) {
+ // Create our own looper here in case MP was created without one
+ mHandlerThread = new HandlerThread("MediaPlayer2MTPEventThread",
+ Process.THREAD_PRIORITY_FOREGROUND);
+ mHandlerThread.start();
+ looper = mHandlerThread.getLooper();
+ }
+ mEventHandler = new EventHandler(looper);
+
+ mListeners = new MediaTimeProvider.OnMediaTimeListener[0];
+ mTimes = new long[0];
+ mLastTimeUs = 0;
+ }
+
+ private void scheduleNotification(int type, long delayUs) {
+ // ignore time notifications until seek is handled
+ if (mSeeking && type == NOTIFY_TIME) {
+ return;
+ }
+
+ if (DEBUG) Log.v(TAG, "scheduleNotification " + type + " in " + delayUs);
+ mEventHandler.removeMessages(NOTIFY);
+ Message msg = mEventHandler.obtainMessage(NOTIFY, type, 0);
+ mEventHandler.sendMessageDelayed(msg, (int) (delayUs / 1000));
+ }
+
+ /** @hide */
+ public void close() {
+ mEventHandler.removeMessages(NOTIFY);
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread = null;
+ }
+ }
+
+ /** @hide */
+ protected void finalize() {
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ }
+ }
+
+ /** @hide */
+ public void onNotifyTime() {
+ synchronized (this) {
+ if (DEBUG) Log.d(TAG, "onNotifyTime: ");
+ scheduleNotification(NOTIFY_TIME, 0 /* delay */);
+ }
+ }
+
+ /** @hide */
+ public void onPaused(boolean paused) {
+ synchronized(this) {
+ if (DEBUG) Log.d(TAG, "onPaused: " + paused);
+ if (mStopped) { // handle as seek if we were stopped
+ mStopped = false;
+ mSeeking = true;
+ scheduleNotification(NOTIFY_SEEK, 0 /* delay */);
+ } else {
+ mPausing = paused; // special handling if player disappeared
+ mSeeking = false;
+ scheduleNotification(NOTIFY_TIME, 0 /* delay */);
+ }
+ }
+ }
+
+ /** @hide */
+ public void onBuffering(boolean buffering) {
+ synchronized (this) {
+ if (DEBUG) Log.d(TAG, "onBuffering: " + buffering);
+ mBuffering = buffering;
+ scheduleNotification(NOTIFY_TIME, 0 /* delay */);
+ }
+ }
+
+ /** @hide */
+ public void onStopped() {
+ synchronized(this) {
+ if (DEBUG) Log.d(TAG, "onStopped");
+ mPaused = true;
+ mStopped = true;
+ mSeeking = false;
+ mBuffering = false;
+ scheduleNotification(NOTIFY_STOP, 0 /* delay */);
+ }
+ }
+
+ /** @hide */
+ public void onSeekComplete(MediaPlayer2Impl mp) {
+ synchronized(this) {
+ mStopped = false;
+ mSeeking = true;
+ scheduleNotification(NOTIFY_SEEK, 0 /* delay */);
+ }
+ }
+
+ /** @hide */
+ public void onNewPlayer() {
+ if (mRefresh) {
+ synchronized(this) {
+ mStopped = false;
+ mSeeking = true;
+ mBuffering = false;
+ scheduleNotification(NOTIFY_SEEK, 0 /* delay */);
+ }
+ }
+ }
+
+ private synchronized void notifySeek() {
+ mSeeking = false;
+ try {
+ long timeUs = getCurrentTimeUs(true, false);
+ if (DEBUG) Log.d(TAG, "onSeekComplete at " + timeUs);
+
+ for (MediaTimeProvider.OnMediaTimeListener listener: mListeners) {
+ if (listener == null) {
+ break;
+ }
+ listener.onSeek(timeUs);
+ }
+ } catch (IllegalStateException e) {
+ // we should not be there, but at least signal pause
+ if (DEBUG) Log.d(TAG, "onSeekComplete but no player");
+ mPausing = true; // special handling if player disappeared
+ notifyTimedEvent(false /* refreshTime */);
+ }
+ }
+
+ private synchronized void notifyTrackData(Pair<SubtitleTrack, byte[]> trackData) {
+ SubtitleTrack track = trackData.first;
+ byte[] data = trackData.second;
+ track.onData(data, true /* eos */, ~0 /* runID: keep forever */);
+ }
+
+ private synchronized void notifyStop() {
+ for (MediaTimeProvider.OnMediaTimeListener listener: mListeners) {
+ if (listener == null) {
+ break;
+ }
+ listener.onStop();
+ }
+ }
+
+ private int registerListener(MediaTimeProvider.OnMediaTimeListener listener) {
+ int i = 0;
+ for (; i < mListeners.length; i++) {
+ if (mListeners[i] == listener || mListeners[i] == null) {
+ break;
+ }
+ }
+
+ // new listener
+ if (i >= mListeners.length) {
+ MediaTimeProvider.OnMediaTimeListener[] newListeners =
+ new MediaTimeProvider.OnMediaTimeListener[i + 1];
+ long[] newTimes = new long[i + 1];
+ System.arraycopy(mListeners, 0, newListeners, 0, mListeners.length);
+ System.arraycopy(mTimes, 0, newTimes, 0, mTimes.length);
+ mListeners = newListeners;
+ mTimes = newTimes;
+ }
+
+ if (mListeners[i] == null) {
+ mListeners[i] = listener;
+ mTimes[i] = MediaTimeProvider.NO_TIME;
+ }
+ return i;
+ }
+
+ public void notifyAt(
+ long timeUs, MediaTimeProvider.OnMediaTimeListener listener) {
+ synchronized(this) {
+ if (DEBUG) Log.d(TAG, "notifyAt " + timeUs);
+ mTimes[registerListener(listener)] = timeUs;
+ scheduleNotification(NOTIFY_TIME, 0 /* delay */);
+ }
+ }
+
+ public void scheduleUpdate(MediaTimeProvider.OnMediaTimeListener listener) {
+ synchronized(this) {
+ if (DEBUG) Log.d(TAG, "scheduleUpdate");
+ int i = registerListener(listener);
+
+ if (!mStopped) {
+ mTimes[i] = 0;
+ scheduleNotification(NOTIFY_TIME, 0 /* delay */);
+ }
+ }
+ }
+
+ public void cancelNotifications(
+ MediaTimeProvider.OnMediaTimeListener listener) {
+ synchronized(this) {
+ int i = 0;
+ for (; i < mListeners.length; i++) {
+ if (mListeners[i] == listener) {
+ System.arraycopy(mListeners, i + 1,
+ mListeners, i, mListeners.length - i - 1);
+ System.arraycopy(mTimes, i + 1,
+ mTimes, i, mTimes.length - i - 1);
+ mListeners[mListeners.length - 1] = null;
+ mTimes[mTimes.length - 1] = NO_TIME;
+ break;
+ } else if (mListeners[i] == null) {
+ break;
+ }
+ }
+
+ scheduleNotification(NOTIFY_TIME, 0 /* delay */);
+ }
+ }
+
+ private synchronized void notifyTimedEvent(boolean refreshTime) {
+ // figure out next callback
+ long nowUs;
+ try {
+ nowUs = getCurrentTimeUs(refreshTime, true);
+ } catch (IllegalStateException e) {
+ // assume we paused until new player arrives
+ mRefresh = true;
+ mPausing = true; // this ensures that call succeeds
+ nowUs = getCurrentTimeUs(refreshTime, true);
+ }
+ long nextTimeUs = nowUs;
+
+ if (mSeeking) {
+ // skip timed-event notifications until seek is complete
+ return;
+ }
+
+ if (DEBUG) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("notifyTimedEvent(").append(mLastTimeUs).append(" -> ")
+ .append(nowUs).append(") from {");
+ boolean first = true;
+ for (long time: mTimes) {
+ if (time == NO_TIME) {
+ continue;
+ }
+ if (!first) sb.append(", ");
+ sb.append(time);
+ first = false;
+ }
+ sb.append("}");
+ Log.d(TAG, sb.toString());
+ }
+
+ Vector<MediaTimeProvider.OnMediaTimeListener> activatedListeners =
+ new Vector<MediaTimeProvider.OnMediaTimeListener>();
+ for (int ix = 0; ix < mTimes.length; ix++) {
+ if (mListeners[ix] == null) {
+ break;
+ }
+ if (mTimes[ix] <= NO_TIME) {
+ // ignore, unless we were stopped
+ } else if (mTimes[ix] <= nowUs + MAX_EARLY_CALLBACK_US) {
+ activatedListeners.add(mListeners[ix]);
+ if (DEBUG) Log.d(TAG, "removed");
+ mTimes[ix] = NO_TIME;
+ } else if (nextTimeUs == nowUs || mTimes[ix] < nextTimeUs) {
+ nextTimeUs = mTimes[ix];
+ }
+ }
+
+ if (nextTimeUs > nowUs && !mPaused) {
+ // schedule callback at nextTimeUs
+ if (DEBUG) Log.d(TAG, "scheduling for " + nextTimeUs + " and " + nowUs);
+ mPlayer.notifyAt(nextTimeUs);
+ } else {
+ mEventHandler.removeMessages(NOTIFY);
+ // no more callbacks
+ }
+
+ for (MediaTimeProvider.OnMediaTimeListener listener: activatedListeners) {
+ listener.onTimedEvent(nowUs);
+ }
+ }
+
+ public long getCurrentTimeUs(boolean refreshTime, boolean monotonic)
+ throws IllegalStateException {
+ synchronized (this) {
+ // we always refresh the time when the paused-state changes, because
+ // we expect to have received the pause-change event delayed.
+ if (mPaused && !refreshTime) {
+ return mLastReportedTime;
+ }
+
+ try {
+ mLastTimeUs = mPlayer.getCurrentPosition() * 1000L;
+ mPaused = !mPlayer.isPlaying() || mBuffering;
+ if (DEBUG) Log.v(TAG, (mPaused ? "paused" : "playing") + " at " + mLastTimeUs);
+ } catch (IllegalStateException e) {
+ if (mPausing) {
+ // if we were pausing, get last estimated timestamp
+ mPausing = false;
+ if (!monotonic || mLastReportedTime < mLastTimeUs) {
+ mLastReportedTime = mLastTimeUs;
+ }
+ mPaused = true;
+ if (DEBUG) Log.d(TAG, "illegal state, but pausing: estimating at " + mLastReportedTime);
+ return mLastReportedTime;
+ }
+ // TODO get time when prepared
+ throw e;
+ }
+ if (monotonic && mLastTimeUs < mLastReportedTime) {
+ /* have to adjust time */
+ if (mLastReportedTime - mLastTimeUs > 1000000) {
+ // schedule seeked event if time jumped significantly
+ // TODO: do this properly by introducing an exception
+ mStopped = false;
+ mSeeking = true;
+ scheduleNotification(NOTIFY_SEEK, 0 /* delay */);
+ }
+ } else {
+ mLastReportedTime = mLastTimeUs;
+ }
+
+ return mLastReportedTime;
+ }
+ }
+
+ private class EventHandler extends Handler {
+ public EventHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == NOTIFY) {
+ switch (msg.arg1) {
+ case NOTIFY_TIME:
+ notifyTimedEvent(true /* refreshTime */);
+ break;
+ case NOTIFY_STOP:
+ notifyStop();
+ break;
+ case NOTIFY_SEEK:
+ notifySeek();
+ break;
+ case NOTIFY_TRACK_DATA:
+ notifyTrackData((Pair<SubtitleTrack, byte[]>)msg.obj);
+ break;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/android/media/MediaPlayerBase.java b/android/media/MediaPlayerBase.java
new file mode 100644
index 0000000..d638a9f
--- /dev/null
+++ b/android/media/MediaPlayerBase.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.media.MediaSession2.PlaylistParam;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Base interfaces for all media players that want media session.
+ *
+ * @hide
+ */
+public abstract class MediaPlayerBase {
+ /**
+ * Listens change in {@link PlaybackState2}.
+ */
+ public interface PlaybackListener {
+ /**
+ * Called when {@link PlaybackState2} for this player is changed.
+ */
+ void onPlaybackChanged(PlaybackState2 state);
+ }
+
+ public abstract void play();
+ public abstract void prepare();
+ public abstract void pause();
+ public abstract void stop();
+ public abstract void skipToPrevious();
+ public abstract void skipToNext();
+ public abstract void seekTo(long pos);
+ public abstract void fastFoward();
+ public abstract void rewind();
+
+ public abstract PlaybackState2 getPlaybackState();
+ public abstract AudioAttributes getAudioAttributes();
+
+ public abstract void setPlaylist(List<MediaItem2> item, PlaylistParam param);
+ public abstract void setCurrentPlaylistItem(int index);
+
+ /**
+ * Add a {@link PlaybackListener} to be invoked when the playback state is changed.
+ *
+ * @param executor the Handler that will receive the listener
+ * @param listener the listener that will be run
+ */
+ public abstract void addPlaybackListener(Executor executor, PlaybackListener listener);
+
+ /**
+ * Remove previously added {@link PlaybackListener}.
+ *
+ * @param listener the listener to be removed
+ */
+ public abstract void removePlaybackListener(PlaybackListener listener);
+}
diff --git a/android/media/MediaRecorder.java b/android/media/MediaRecorder.java
index 3c49b80..78477f7 100644
--- a/android/media/MediaRecorder.java
+++ b/android/media/MediaRecorder.java
@@ -1380,7 +1380,8 @@
if (listener != null && !mRoutingChangeListeners.containsKey(listener)) {
enableNativeRoutingCallbacksLocked(true);
mRoutingChangeListeners.put(
- listener, new NativeRoutingEventHandlerDelegate(this, listener, handler));
+ listener, new NativeRoutingEventHandlerDelegate(this, listener,
+ handler != null ? handler : mEventHandler));
}
}
}
@@ -1401,36 +1402,6 @@
}
}
- /**
- * Helper class to handle the forwarding of native events to the appropriate listener
- * (potentially) handled in a different thread
- */
- private class NativeRoutingEventHandlerDelegate {
- private MediaRecorder mMediaRecorder;
- private AudioRouting.OnRoutingChangedListener mOnRoutingChangedListener;
- private Handler mHandler;
-
- NativeRoutingEventHandlerDelegate(final MediaRecorder mediaRecorder,
- final AudioRouting.OnRoutingChangedListener listener, Handler handler) {
- mMediaRecorder = mediaRecorder;
- mOnRoutingChangedListener = listener;
- mHandler = handler != null ? handler : mEventHandler;
- }
-
- void notifyClient() {
- if (mHandler != null) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- if (mOnRoutingChangedListener != null) {
- mOnRoutingChangedListener.onRoutingChanged(mMediaRecorder);
- }
- }
- });
- }
- }
- }
-
private native final boolean native_setInputDevice(int deviceId);
private native final int native_getRoutedDeviceId();
private native final void native_enableDeviceCallback(boolean enabled);
diff --git a/android/media/MediaSession2.java b/android/media/MediaSession2.java
new file mode 100644
index 0000000..0e90040
--- /dev/null
+++ b/android/media/MediaSession2.java
@@ -0,0 +1,1223 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.session.MediaSession;
+import android.media.session.MediaSession.Callback;
+import android.media.session.PlaybackState;
+import android.media.update.ApiLoader;
+import android.media.update.MediaSession2Provider;
+import android.media.update.MediaSession2Provider.ControllerInfoProvider;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcelable;
+import android.os.ResultReceiver;
+import android.text.TextUtils;
+import android.util.ArraySet;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Allows a media app to expose its transport controls and playback information in a process to
+ * other processes including the Android framework and other apps. Common use cases are as follows.
+ * <ul>
+ * <li>Bluetooth/wired headset key events support</li>
+ * <li>Android Auto/Wearable support</li>
+ * <li>Separating UI process and playback process</li>
+ * </ul>
+ * <p>
+ * A MediaSession2 should be created when an app wants to publish media playback information or
+ * handle media keys. In general an app only needs one session for all playback, though multiple
+ * sessions can be created to provide finer grain controls of media.
+ * <p>
+ * If you want to support background playback, {@link MediaSessionService2} is preferred
+ * instead. With it, your playback can be revived even after you've finished playback. See
+ * {@link MediaSessionService2} for details.
+ * <p>
+ * A session can be obtained by {@link Builder}. The owner of the session may pass its session token
+ * to other processes to allow them to create a {@link MediaController2} to interact with the
+ * session.
+ * <p>
+ * When a session receive transport control commands, the session sends the commands directly to
+ * the the underlying media player set by {@link Builder} or {@link #setPlayer(MediaPlayerBase)}.
+ * <p>
+ * When an app is finished performing playback it must call {@link #close()} to clean up the session
+ * and notify any controllers.
+ * <p>
+ * {@link MediaSession2} objects should be used on the thread on the looper.
+ *
+ * @see MediaSessionService2
+ * @hide
+ */
+// TODO(jaewan): Unhide
+// TODO(jaewan): Revisit comments. Currently it's borrowed from the MediaSession.
+// TODO(jaewan): Should we support thread safe? It may cause tricky issue such as b/63797089
+// TODO(jaewan): Should we make APIs for MediaSessionService2 public? It's helpful for
+// developers that doesn't want to override from Browser, but user may not use this
+// correctly.
+public class MediaSession2 implements AutoCloseable {
+ private final MediaSession2Provider mProvider;
+
+ // Note: Do not define IntDef because subclass can add more command code on top of these.
+ // TODO(jaewan): Shouldn't we pull out?
+ public static final int COMMAND_CODE_CUSTOM = 0;
+ public static final int COMMAND_CODE_PLAYBACK_START = 1;
+ public static final int COMMAND_CODE_PLAYBACK_PAUSE = 2;
+ public static final int COMMAND_CODE_PLAYBACK_STOP = 3;
+ public static final int COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM = 4;
+ public static final int COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM = 5;
+ public static final int COMMAND_CODE_PLAYBACK_PREPARE = 6;
+ public static final int COMMAND_CODE_PLAYBACK_FAST_FORWARD = 7;
+ public static final int COMMAND_CODE_PLAYBACK_REWIND = 8;
+ public static final int COMMAND_CODE_PLAYBACK_SEEK_TO = 9;
+ public static final int COMMAND_CODE_PLAYBACK_SET_CURRENT_PLAYLIST_ITEM = 10;
+
+ public static final int COMMAND_CODE_PLAYLIST_GET = 11;
+ public static final int COMMAND_CODE_PLAYLIST_ADD = 12;
+ public static final int COMMAND_CODE_PLAYLIST_REMOVE = 13;
+
+ public static final int COMMAND_CODE_PLAY_FROM_MEDIA_ID = 14;
+ public static final int COMMAND_CODE_PLAY_FROM_URI = 15;
+ public static final int COMMAND_CODE_PLAY_FROM_SEARCH = 16;
+
+ public static final int COMMAND_CODE_PREPARE_FROM_MEDIA_ID = 17;
+ public static final int COMMAND_CODE_PREPARE_FROM_URI = 18;
+ public static final int COMMAND_CODE_PREPARE_FROM_SEARCH = 19;
+
+ /**
+ * Define a command that a {@link MediaController2} can send to a {@link MediaSession2}.
+ * <p>
+ * If {@link #getCommandCode()} isn't {@link #COMMAND_CODE_CUSTOM}), it's predefined command.
+ * If {@link #getCommandCode()} is {@link #COMMAND_CODE_CUSTOM}), it's custom command and
+ * {@link #getCustomCommand()} shouldn't be {@code null}.
+ */
+ // TODO(jaewan): Move this into the updatable.
+ public static final class Command {
+ private static final String KEY_COMMAND_CODE
+ = "android.media.media_session2.command.command_code";
+ private static final String KEY_COMMAND_CUSTOM_COMMAND
+ = "android.media.media_session2.command.custom_command";
+ private static final String KEY_COMMAND_EXTRA
+ = "android.media.media_session2.command.extra";
+
+ private final int mCommandCode;
+ // Nonnull if it's custom command
+ private final String mCustomCommand;
+ private final Bundle mExtra;
+
+ public Command(int commandCode) {
+ mCommandCode = commandCode;
+ mCustomCommand = null;
+ mExtra = null;
+ }
+
+ public Command(@NonNull String action, @Nullable Bundle extra) {
+ if (action == null) {
+ throw new IllegalArgumentException("action shouldn't be null");
+ }
+ mCommandCode = COMMAND_CODE_CUSTOM;
+ mCustomCommand = action;
+ mExtra = extra;
+ }
+
+ public int getCommandCode() {
+ return mCommandCode;
+ }
+
+ public @Nullable String getCustomCommand() {
+ return mCustomCommand;
+ }
+
+ public @Nullable Bundle getExtra() {
+ return mExtra;
+ }
+
+ /**
+ * @return a new Bundle instance from the Command
+ * @hide
+ */
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(KEY_COMMAND_CODE, mCommandCode);
+ bundle.putString(KEY_COMMAND_CUSTOM_COMMAND, mCustomCommand);
+ bundle.putBundle(KEY_COMMAND_EXTRA, mExtra);
+ return bundle;
+ }
+
+ /**
+ * @return a new Command instance from the Bundle
+ * @hide
+ */
+ public static Command fromBundle(Bundle command) {
+ int code = command.getInt(KEY_COMMAND_CODE);
+ if (code != COMMAND_CODE_CUSTOM) {
+ return new Command(code);
+ } else {
+ String customCommand = command.getString(KEY_COMMAND_CUSTOM_COMMAND);
+ if (customCommand == null) {
+ return null;
+ }
+ return new Command(customCommand, command.getBundle(KEY_COMMAND_EXTRA));
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Command)) {
+ return false;
+ }
+ Command other = (Command) obj;
+ // TODO(jaewan): Should we also compare contents in bundle?
+ // It may not be possible if the bundle contains private class.
+ return mCommandCode == other.mCommandCode
+ && TextUtils.equals(mCustomCommand, other.mCustomCommand);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ return ((mCustomCommand != null) ? mCustomCommand.hashCode() : 0) * prime + mCommandCode;
+ }
+ }
+
+ /**
+ * Represent set of {@link Command}.
+ */
+ // TODO(jaewan): Move this to updatable
+ public static class CommandGroup {
+ private static final String KEY_COMMANDS =
+ "android.media.mediasession2.commandgroup.commands";
+ private ArraySet<Command> mCommands = new ArraySet<>();
+
+ public CommandGroup() {
+ }
+
+ public CommandGroup(CommandGroup others) {
+ mCommands.addAll(others.mCommands);
+ }
+
+ public void addCommand(Command command) {
+ mCommands.add(command);
+ }
+
+ public void addAllPredefinedCommands() {
+ // TODO(jaewan): Is there any better way than this?
+ mCommands.add(new Command(COMMAND_CODE_PLAYBACK_START));
+ mCommands.add(new Command(COMMAND_CODE_PLAYBACK_PAUSE));
+ mCommands.add(new Command(COMMAND_CODE_PLAYBACK_STOP));
+ mCommands.add(new Command(COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM));
+ mCommands.add(new Command(COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM));
+ }
+
+ public void removeCommand(Command command) {
+ mCommands.remove(command);
+ }
+
+ public boolean hasCommand(Command command) {
+ return mCommands.contains(command);
+ }
+
+ public boolean hasCommand(int code) {
+ if (code == COMMAND_CODE_CUSTOM) {
+ throw new IllegalArgumentException("Use hasCommand(Command) for custom command");
+ }
+ for (int i = 0; i < mCommands.size(); i++) {
+ if (mCommands.valueAt(i).getCommandCode() == code) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return new bundle from the CommandGroup
+ * @hide
+ */
+ public Bundle toBundle() {
+ ArrayList<Bundle> list = new ArrayList<>();
+ for (int i = 0; i < mCommands.size(); i++) {
+ list.add(mCommands.valueAt(i).toBundle());
+ }
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArrayList(KEY_COMMANDS, list);
+ return bundle;
+ }
+
+ /**
+ * @return new instance of CommandGroup from the bundle
+ * @hide
+ */
+ public static @Nullable CommandGroup fromBundle(Bundle commands) {
+ if (commands == null) {
+ return null;
+ }
+ List<Parcelable> list = commands.getParcelableArrayList(KEY_COMMANDS);
+ if (list == null) {
+ return null;
+ }
+ CommandGroup commandGroup = new CommandGroup();
+ for (int i = 0; i < list.size(); i++) {
+ Parcelable parcelable = list.get(i);
+ if (!(parcelable instanceof Bundle)) {
+ continue;
+ }
+ Bundle commandBundle = (Bundle) parcelable;
+ Command command = Command.fromBundle(commandBundle);
+ if (command != null) {
+ commandGroup.addCommand(command);
+ }
+ }
+ return commandGroup;
+ }
+ }
+
+ /**
+ * Callback to be called for all incoming commands from {@link MediaController2}s.
+ * <p>
+ * If it's not set, the session will accept all controllers and all incoming commands by
+ * default.
+ */
+ // TODO(jaewan): Can we move this inside of the updatable for default implementation.
+ public static class SessionCallback {
+ /**
+ * Called when a controller is created for this session. Return allowed commands for
+ * controller. By default it allows all connection requests and commands.
+ * <p>
+ * You can reject the connection by return {@code null}. In that case, controller receives
+ * {@link MediaController2.ControllerCallback#onDisconnected()} and cannot be usable.
+ *
+ * @param controller controller information.
+ * @return allowed commands. Can be {@code null} to reject coonnection.
+ */
+ // TODO(jaewan): Change return type. Once we do, null is for reject.
+ public @Nullable CommandGroup onConnect(@NonNull ControllerInfo controller) {
+ CommandGroup commands = new CommandGroup();
+ commands.addAllPredefinedCommands();
+ return commands;
+ }
+
+ /**
+ * Called when a controller is disconnected
+ *
+ * @param controller controller information
+ */
+ public void onDisconnected(@NonNull ControllerInfo controller) { }
+
+ /**
+ * Called when a controller sent a command to the session, and the command will be sent to
+ * the player directly unless you reject the request by {@code false}.
+ *
+ * @param controller controller information.
+ * @param command a command. This method will be called for every single command.
+ * @return {@code true} if you want to accept incoming command. {@code false} otherwise.
+ */
+ // TODO(jaewan): Add more documentations (or make it clear) which commands can be filtered
+ // with this.
+ public boolean onCommandRequest(@NonNull ControllerInfo controller,
+ @NonNull Command command) {
+ return true;
+ }
+
+ /**
+ * Called when a controller set rating on the currently playing contents.
+ *
+ * @param
+ */
+ public void onSetRating(@NonNull ControllerInfo controller, @NonNull Rating2 rating) { }
+
+ /**
+ * Called when a controller sent a custom command.
+ *
+ * @param controller controller information
+ * @param customCommand custom command.
+ * @param args optional arguments
+ * @param cb optional result receiver
+ */
+ public void onCustomCommand(@NonNull ControllerInfo controller,
+ @NonNull Command customCommand, @Nullable Bundle args,
+ @Nullable ResultReceiver cb) { }
+
+ /**
+ * Override to handle requests to prepare for playing a specific mediaId.
+ * During the preparation, a session should not hold audio focus in order to allow other
+ * sessions play seamlessly. The state of playback should be updated to
+ * {@link PlaybackState#STATE_PAUSED} after the preparation is done.
+ * <p>
+ * The playback of the prepared content should start in the later calls of
+ * {@link MediaSession2#play()}.
+ * <p>
+ * Override {@link #onPlayFromMediaId} to handle requests for starting
+ * playback without preparation.
+ */
+ public void onPlayFromMediaId(@NonNull ControllerInfo controller,
+ @NonNull String mediaId, @Nullable Bundle extras) { }
+
+ /**
+ * Override to handle requests to prepare playback from a search query. An empty query
+ * indicates that the app may prepare any music. The implementation should attempt to make a
+ * smart choice about what to play. During the preparation, a session should not hold audio
+ * focus in order to allow other sessions play seamlessly. The state of playback should be
+ * updated to {@link PlaybackState#STATE_PAUSED} after the preparation is done.
+ * <p>
+ * The playback of the prepared content should start in the later calls of
+ * {@link MediaSession2#play()}.
+ * <p>
+ * Override {@link #onPlayFromSearch} to handle requests for starting playback without
+ * preparation.
+ */
+ public void onPlayFromSearch(@NonNull ControllerInfo controller,
+ @NonNull String query, @Nullable Bundle extras) { }
+
+ /**
+ * Override to handle requests to prepare a specific media item represented by a URI.
+ * During the preparation, a session should not hold audio focus in order to allow
+ * other sessions play seamlessly. The state of playback should be updated to
+ * {@link PlaybackState#STATE_PAUSED} after the preparation is done.
+ * <p>
+ * The playback of the prepared content should start in the later calls of
+ * {@link MediaSession2#play()}.
+ * <p>
+ * Override {@link #onPlayFromUri} to handle requests for starting playback without
+ * preparation.
+ */
+ public void onPlayFromUri(@NonNull ControllerInfo controller,
+ @NonNull String uri, @Nullable Bundle extras) { }
+
+ /**
+ * Override to handle requests to play a specific mediaId.
+ */
+ public void onPrepareFromMediaId(@NonNull ControllerInfo controller,
+ @NonNull String mediaId, @Nullable Bundle extras) { }
+
+ /**
+ * Override to handle requests to begin playback from a search query. An
+ * empty query indicates that the app may play any music. The
+ * implementation should attempt to make a smart choice about what to
+ * play.
+ */
+ public void onPrepareFromSearch(@NonNull ControllerInfo controller,
+ @NonNull String query, @Nullable Bundle extras) { }
+
+ /**
+ * Override to handle requests to play a specific media item represented by a URI.
+ */
+ public void prepareFromUri(@NonNull ControllerInfo controller,
+ @NonNull Uri uri, @Nullable Bundle extras) { }
+
+ /**
+ * Called when a controller wants to add a {@link MediaItem2} at the specified position
+ * in the play queue.
+ * <p>
+ * The item from the media controller wouldn't have valid data source descriptor because
+ * it would have been anonymized when it's sent to the remote process.
+ *
+ * @param item The media item to be inserted.
+ * @param index The index at which the item is to be inserted.
+ */
+ public void onAddPlaylistItem(@NonNull ControllerInfo controller,
+ @NonNull MediaItem2 item, int index) { }
+
+ /**
+ * Called when a controller wants to remove the {@link MediaItem2}
+ *
+ * @param item
+ */
+ // Can we do this automatically?
+ public void onRemovePlaylistItem(@NonNull MediaItem2 item) { }
+ };
+
+ /**
+ * Base builder class for MediaSession2 and its subclass.
+ *
+ * @hide
+ */
+ static abstract class BuilderBase
+ <T extends MediaSession2.BuilderBase<T, C>, C extends SessionCallback> {
+ final Context mContext;
+ final MediaPlayerBase mPlayer;
+ String mId;
+ Executor mCallbackExecutor;
+ C mCallback;
+ VolumeProvider mVolumeProvider;
+ int mRatingType;
+ PendingIntent mSessionActivity;
+
+ /**
+ * Constructor.
+ *
+ * @param context a context
+ * @param player a player to handle incoming command from any controller.
+ * @throws IllegalArgumentException if any parameter is null, or the player is a
+ * {@link MediaSession2} or {@link MediaController2}.
+ */
+ // TODO(jaewan): Also need executor
+ public BuilderBase(@NonNull Context context, @NonNull MediaPlayerBase player) {
+ if (context == null) {
+ throw new IllegalArgumentException("context shouldn't be null");
+ }
+ if (player == null) {
+ throw new IllegalArgumentException("player shouldn't be null");
+ }
+ mContext = context;
+ mPlayer = player;
+ // Ensure non-null
+ mId = "";
+ }
+
+ /**
+ * Set volume provider to configure this session to use remote volume handling.
+ * This must be called to receive volume button events, otherwise the system
+ * will adjust the appropriate stream volume for this session's player.
+ * <p>
+ * Set {@code null} to reset.
+ *
+ * @param volumeProvider The provider that will handle volume changes. Can be {@code null}
+ */
+ public T setVolumeProvider(@Nullable VolumeProvider volumeProvider) {
+ mVolumeProvider = volumeProvider;
+ return (T) this;
+ }
+
+ /**
+ * Set the style of rating used by this session. Apps trying to set the
+ * rating should use this style. Must be one of the following:
+ * <ul>
+ * <li>{@link Rating2#RATING_NONE}</li>
+ * <li>{@link Rating2#RATING_3_STARS}</li>
+ * <li>{@link Rating2#RATING_4_STARS}</li>
+ * <li>{@link Rating2#RATING_5_STARS}</li>
+ * <li>{@link Rating2#RATING_HEART}</li>
+ * <li>{@link Rating2#RATING_PERCENTAGE}</li>
+ * <li>{@link Rating2#RATING_THUMB_UP_DOWN}</li>
+ * </ul>
+ */
+ public T setRatingType(@Rating2.Style int type) {
+ mRatingType = type;
+ return (T) this;
+ }
+
+ /**
+ * Set an intent for launching UI for this Session. This can be used as a
+ * quick link to an ongoing media screen. The intent should be for an
+ * activity that may be started using {@link Activity#startActivity(Intent)}.
+ *
+ * @param pi The intent to launch to show UI for this session.
+ */
+ public T setSessionActivity(@Nullable PendingIntent pi) {
+ mSessionActivity = pi;
+ return (T) this;
+ }
+
+ /**
+ * Set ID of the session. If it's not set, an empty string with used to create a session.
+ * <p>
+ * Use this if and only if your app supports multiple playback at the same time and also
+ * wants to provide external apps to have finer controls of them.
+ *
+ * @param id id of the session. Must be unique per package.
+ * @throws IllegalArgumentException if id is {@code null}
+ * @return
+ */
+ public T setId(@NonNull String id) {
+ if (id == null) {
+ throw new IllegalArgumentException("id shouldn't be null");
+ }
+ mId = id;
+ return (T) this;
+ }
+
+ /**
+ * Set {@link SessionCallback}.
+ *
+ * @param executor callback executor
+ * @param callback session callback.
+ * @return
+ */
+ public T setSessionCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull C callback) {
+ if (executor == null) {
+ throw new IllegalArgumentException("executor shouldn't be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback shouldn't be null");
+ }
+ mCallbackExecutor = executor;
+ mCallback = callback;
+ return (T) this;
+ }
+
+ /**
+ * Build {@link MediaSession2}.
+ *
+ * @return a new session
+ * @throws IllegalStateException if the session with the same id is already exists for the
+ * package.
+ */
+ public abstract MediaSession2 build() throws IllegalStateException;
+ }
+
+ /**
+ * Builder for {@link MediaSession2}.
+ * <p>
+ * Any incoming event from the {@link MediaController2} will be handled on the thread
+ * that created session with the {@link Builder#build()}.
+ */
+ // TODO(jaewan): Move this to updatable
+ // TODO(jaewan): Add setRatingType()
+ // TODO(jaewan): Add setSessionActivity()
+ public static final class Builder extends BuilderBase<Builder, SessionCallback> {
+ public Builder(Context context, @NonNull MediaPlayerBase player) {
+ super(context, player);
+ }
+
+ @Override
+ public MediaSession2 build() throws IllegalStateException {
+ if (mCallback == null) {
+ mCallback = new SessionCallback();
+ }
+ return new MediaSession2(mContext, mPlayer, mId, mCallbackExecutor, mCallback,
+ mVolumeProvider, mRatingType, mSessionActivity);
+ }
+ }
+
+ /**
+ * Information of a controller.
+ */
+ // TODO(jaewan): Move implementation to the updatable.
+ public static final class ControllerInfo {
+ private final ControllerInfoProvider mProvider;
+
+ /**
+ * @hide
+ */
+ // TODO(jaewan): SystemApi
+ // TODO(jaewan): Also accept componentName to check notificaiton listener.
+ public ControllerInfo(Context context, int uid, int pid, String packageName,
+ IMediaSession2Callback callback) {
+ mProvider = ApiLoader.getProvider(context)
+ .createMediaSession2ControllerInfoProvider(
+ this, context, uid, pid, packageName, callback);
+ }
+
+ /**
+ * @return package name of the controller
+ */
+ public String getPackageName() {
+ return mProvider.getPackageName_impl();
+ }
+
+ /**
+ * @return uid of the controller
+ */
+ public int getUid() {
+ return mProvider.getUid_impl();
+ }
+
+ /**
+ * Return if the controller has granted {@code android.permission.MEDIA_CONTENT_CONTROL} or
+ * has a enabled notification listener so can be trusted to accept connection and incoming
+ * command request.
+ *
+ * @return {@code true} if the controller is trusted.
+ */
+ public boolean isTrusted() {
+ return mProvider.isTrusted_impl();
+ }
+
+ /**
+ * @hide
+ * @return
+ */
+ // TODO(jaewan): SystemApi
+ public ControllerInfoProvider getProvider() {
+ return mProvider;
+ }
+
+ @Override
+ public int hashCode() {
+ return mProvider.hashCode_impl();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ControllerInfo)) {
+ return false;
+ }
+ ControllerInfo other = (ControllerInfo) obj;
+ return mProvider.equals_impl(other.mProvider);
+ }
+
+ @Override
+ public String toString() {
+ // TODO(jaewan): Move this to updatable.
+ return "ControllerInfo {pkg=" + getPackageName() + ", uid=" + getUid() + ", trusted="
+ + isTrusted() + "}";
+ }
+ }
+
+ /**
+ * Button for a {@link Command} that will be shown by the controller.
+ * <p>
+ * It's up to the controller's decision to respect or ignore this customization request.
+ */
+ // TODO(jaewan): Move this to updatable.
+ public static class CommandButton {
+ private static final String KEY_COMMAND
+ = "android.media.media_session2.command_button.command";
+ private static final String KEY_ICON_RES_ID
+ = "android.media.media_session2.command_button.icon_res_id";
+ private static final String KEY_DISPLAY_NAME
+ = "android.media.media_session2.command_button.display_name";
+ private static final String KEY_EXTRA
+ = "android.media.media_session2.command_button.extra";
+ private static final String KEY_ENABLED
+ = "android.media.media_session2.command_button.enabled";
+
+ private Command mCommand;
+ private int mIconResId;
+ private String mDisplayName;
+ private Bundle mExtra;
+ private boolean mEnabled;
+
+ private CommandButton(@Nullable Command command, int iconResId,
+ @Nullable String displayName, Bundle extra, boolean enabled) {
+ mCommand = command;
+ mIconResId = iconResId;
+ mDisplayName = displayName;
+ mExtra = extra;
+ mEnabled = enabled;
+ }
+
+ /**
+ * Get command associated with this button. Can be {@code null} if the button isn't enabled
+ * and only providing placeholder.
+ *
+ * @return command or {@code null}
+ */
+ public @Nullable Command getCommand() {
+ return mCommand;
+ }
+
+ /**
+ * Resource id of the button in this package. Can be {@code 0} if the command is predefined
+ * and custom icon isn't needed.
+ *
+ * @return resource id of the icon. Can be {@code 0}.
+ */
+ public int getIconResId() {
+ return mIconResId;
+ }
+
+ /**
+ * Display name of the button. Can be {@code null} or empty if the command is predefined
+ * and custom name isn't needed.
+ *
+ * @return custom display name. Can be {@code null} or empty.
+ */
+ public @Nullable String getDisplayName() {
+ return mDisplayName;
+ }
+
+ /**
+ * Extra information of the button. It's private information between session and controller.
+ *
+ * @return
+ */
+ public @Nullable Bundle getExtra() {
+ return mExtra;
+ }
+
+ /**
+ * Return whether it's enabled
+ *
+ * @return {@code true} if enabled. {@code false} otherwise.
+ */
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ /**
+ * @hide
+ */
+ // TODO(jaewan): @SystemApi
+ public @NonNull Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(KEY_COMMAND, mCommand.toBundle());
+ bundle.putInt(KEY_ICON_RES_ID, mIconResId);
+ bundle.putString(KEY_DISPLAY_NAME, mDisplayName);
+ bundle.putBundle(KEY_EXTRA, mExtra);
+ bundle.putBoolean(KEY_ENABLED, mEnabled);
+ return bundle;
+ }
+
+ /**
+ * @hide
+ */
+ // TODO(jaewan): @SystemApi
+ public static @Nullable CommandButton fromBundle(Bundle bundle) {
+ Builder builder = new Builder();
+ builder.setCommand(Command.fromBundle(bundle.getBundle(KEY_COMMAND)));
+ builder.setIconResId(bundle.getInt(KEY_ICON_RES_ID, 0));
+ builder.setDisplayName(bundle.getString(KEY_DISPLAY_NAME));
+ builder.setExtra(bundle.getBundle(KEY_EXTRA));
+ builder.setEnabled(bundle.getBoolean(KEY_ENABLED));
+ try {
+ return builder.build();
+ } catch (IllegalStateException e) {
+ // Malformed or version mismatch. Return null for now.
+ return null;
+ }
+ }
+
+ /**
+ * Builder for {@link CommandButton}.
+ */
+ public static class Builder {
+ private Command mCommand;
+ private int mIconResId;
+ private String mDisplayName;
+ private Bundle mExtra;
+ private boolean mEnabled;
+
+ public Builder() {
+ mEnabled = true;
+ }
+
+ public Builder setCommand(Command command) {
+ mCommand = command;
+ return this;
+ }
+
+ public Builder setIconResId(int resId) {
+ mIconResId = resId;
+ return this;
+ }
+
+ public Builder setDisplayName(String displayName) {
+ mDisplayName = displayName;
+ return this;
+ }
+
+ public Builder setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ return this;
+ }
+
+ public Builder setExtra(Bundle extra) {
+ mExtra = extra;
+ return this;
+ }
+
+ public CommandButton build() {
+ if (mEnabled && mCommand == null) {
+ throw new IllegalStateException("Enabled button needs Command"
+ + " for controller to invoke the command");
+ }
+ if (mCommand != null && mCommand.getCommandCode() == COMMAND_CODE_CUSTOM
+ && (mIconResId == 0 || TextUtils.isEmpty(mDisplayName))) {
+ throw new IllegalStateException("Custom commands needs icon and"
+ + " and name to display");
+ }
+ return new CommandButton(mCommand, mIconResId, mDisplayName, mExtra, mEnabled);
+ }
+ }
+ }
+
+ /**
+ * Parameter for the playlist.
+ */
+ // TODO(jaewan): add fromBundle()/toBundle()
+ public static class PlaylistParam {
+ /**
+ * @hide
+ */
+ @IntDef({REPEAT_MODE_NONE, REPEAT_MODE_ONE, REPEAT_MODE_ALL,
+ REPEAT_MODE_GROUP})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RepeatMode {}
+
+ /**
+ * Playback will be stopped at the end of the playing media list.
+ */
+ public static final int REPEAT_MODE_NONE = 0;
+
+ /**
+ * Playback of the current playing media item will be repeated.
+ */
+ public static final int REPEAT_MODE_ONE = 1;
+
+ /**
+ * Playing media list will be repeated.
+ */
+ public static final int REPEAT_MODE_ALL = 2;
+
+ /**
+ * Playback of the playing media group will be repeated.
+ * A group is a logical block of media items which is specified in the section 5.7 of the
+ * Bluetooth AVRCP 1.6.
+ */
+ public static final int REPEAT_MODE_GROUP = 3;
+
+ /**
+ * @hide
+ */
+ @IntDef({SHUFFLE_MODE_NONE, SHUFFLE_MODE_ALL, SHUFFLE_MODE_GROUP})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ShuffleMode {}
+
+ /**
+ * Media list will be played in order.
+ */
+ public static final int SHUFFLE_MODE_NONE = 0;
+
+ /**
+ * Media list will be played in shuffled order.
+ */
+ public static final int SHUFFLE_MODE_ALL = 1;
+
+ /**
+ * Media group will be played in shuffled order.
+ * A group is a logical block of media items which is specified in the section 5.7 of the
+ * Bluetooth AVRCP 1.6.
+ */
+ public static final int SHUFFLE_MODE_GROUP = 2;
+
+ private @RepeatMode int mRepeatMode;
+ private @ShuffleMode int mShuffleMode;
+
+ private MediaMetadata2 mPlaylistMetadata;
+
+ public PlaylistParam(@RepeatMode int repeatMode, @ShuffleMode int shuffleMode,
+ @Nullable MediaMetadata2 playlistMetadata) {
+ mRepeatMode = repeatMode;
+ mShuffleMode = shuffleMode;
+ mPlaylistMetadata = playlistMetadata;
+ }
+
+ public @RepeatMode int getRepeatMode() {
+ return mRepeatMode;
+ }
+
+ public @ShuffleMode int getShuffleMode() {
+ return mShuffleMode;
+ }
+
+ public MediaMetadata2 getPlaylistMetadata() {
+ return mPlaylistMetadata;
+ }
+ }
+
+ /**
+ * Constructor is hidden and apps can only instantiate indirectly through {@link Builder}.
+ * <p>
+ * This intended behavior and here's the reasons.
+ * 1. Prevent multiple sessions with the same tag in a media app.
+ * Whenever it happens only one session was properly setup and others were all dummies.
+ * Android framework couldn't find the right session to dispatch media key event.
+ * 2. Simplify session's lifecycle.
+ * {@link MediaSession} can be available after all of {@link MediaSession#setFlags(int)},
+ * {@link MediaSession#setCallback(Callback)}, and
+ * {@link MediaSession#setActive(boolean)}. It was common for an app to omit one, so
+ * framework had to add heuristics to figure out if an app is
+ * @hide
+ */
+ MediaSession2(Context context, MediaPlayerBase player, String id, Executor callbackExecutor,
+ SessionCallback callback, VolumeProvider volumeProvider, int ratingType,
+ PendingIntent sessionActivity) {
+ super();
+ mProvider = createProvider(context, player, id, callbackExecutor, callback,
+ volumeProvider, ratingType, sessionActivity);
+ }
+
+ MediaSession2Provider createProvider(Context context, MediaPlayerBase player, String id,
+ Executor callbackExecutor, SessionCallback callback, VolumeProvider volumeProvider,
+ int ratingType, PendingIntent sessionActivity) {
+ return ApiLoader.getProvider(context)
+ .createMediaSession2(this, context, player, id, callbackExecutor,
+ callback, volumeProvider, ratingType, sessionActivity);
+ }
+
+ /**
+ * @hide
+ */
+ // TODO(jaewan): SystemApi
+ public MediaSession2Provider getProvider() {
+ return mProvider;
+ }
+
+ /**
+ * Set the underlying {@link MediaPlayerBase} for this session to dispatch incoming event to.
+ * Events from the {@link MediaController2} will be sent directly to the underlying
+ * player on the {@link Handler} where the session is created on.
+ * <p>
+ * If the new player is successfully set, {@link PlaybackListener}
+ * will be called to tell the current playback state of the new player.
+ * <p>
+ * You can also specify a volume provider. If so, playback in the player is considered as
+ * remote playback.
+ *
+ * @param player a {@link MediaPlayerBase} that handles actual media playback in your app.
+ * @throws IllegalArgumentException if the player is {@code null}.
+ */
+ public void setPlayer(@NonNull MediaPlayerBase player) {
+ mProvider.setPlayer_impl(player);
+ }
+
+ /**
+ * Set the underlying {@link MediaPlayerBase} with the volume provider for remote playback.
+ *
+ * @param player a {@link MediaPlayerBase} that handles actual media playback in your app.
+ * @param volumeProvider a volume provider
+ * @see #setPlayer(MediaPlayerBase)
+ * @see Builder#setVolumeProvider(VolumeProvider)
+ * @throws IllegalArgumentException if a parameter is {@code null}.
+ */
+ public void setPlayer(@NonNull MediaPlayerBase player, @NonNull VolumeProvider volumeProvider)
+ throws IllegalArgumentException {
+ mProvider.setPlayer_impl(player, volumeProvider);
+ }
+
+ @Override
+ public void close() {
+ mProvider.close_impl();
+ }
+
+ /**
+ * @return player
+ */
+ public @Nullable MediaPlayerBase getPlayer() {
+ return mProvider.getPlayer_impl();
+ }
+
+ /**
+ * Returns the {@link SessionToken2} for creating {@link MediaController2}.
+ */
+ public @NonNull
+ SessionToken2 getToken() {
+ return mProvider.getToken_impl();
+ }
+
+ public @NonNull List<ControllerInfo> getConnectedControllers() {
+ return mProvider.getConnectedControllers_impl();
+ }
+
+ /**
+ * Sets the {@link AudioAttributes} to be used during the playback of the video.
+ *
+ * @param attributes non-null <code>AudioAttributes</code>.
+ */
+ public void setAudioAttributes(@NonNull AudioAttributes attributes) {
+ mProvider.setAudioAttributes_impl(attributes);
+ }
+
+ /**
+ * Sets which type of audio focus will be requested during the playback, or configures playback
+ * to not request audio focus. Valid values for focus requests are
+ * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use
+ * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be
+ * requested when playback starts. You can for instance use this when playing a silent animation
+ * through this class, and you don't want to affect other audio applications playing in the
+ * background.
+ *
+ * @param focusGain the type of audio focus gain that will be requested, or
+ * {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during
+ * playback.
+ */
+ public void setAudioFocusRequest(int focusGain) {
+ mProvider.setAudioFocusRequest_impl(focusGain);
+ }
+
+ /**
+ * Sets ordered list of {@link CommandButton} for controllers to build UI with it.
+ * <p>
+ * It's up to controller's decision how to represent the layout in its own UI.
+ * Here's the same way
+ * (layout[i] means a CommandButton at index i in the given list)
+ * For 5 icons row
+ * layout[3] layout[1] layout[0] layout[2] layout[4]
+ * For 3 icons row
+ * layout[1] layout[0] layout[2]
+ * For 5 icons row with overflow icon (can show +5 extra buttons with overflow button)
+ * expanded row: layout[5] layout[6] layout[7] layout[8] layout[9]
+ * main row: layout[3] layout[1] layout[0] layout[2] layout[4]
+ * <p>
+ * This API can be called in the {@link SessionCallback#onConnect(ControllerInfo)}.
+ *
+ * @param controller controller to specify layout.
+ * @param layout oredered list of layout.
+ */
+ public void setCustomLayout(@NonNull ControllerInfo controller,
+ @NonNull List<CommandButton> layout) {
+ mProvider.setCustomLayout_impl(controller, layout);
+ }
+
+ /**
+ * Set the new allowed command group for the controller
+ *
+ * @param controller controller to change allowed commands
+ * @param commands new allowed commands
+ */
+ public void setAllowedCommands(@NonNull ControllerInfo controller,
+ @NonNull CommandGroup commands) {
+ mProvider.setAllowedCommands_impl(controller, commands);
+ }
+
+ /**
+ * Notify changes in metadata of previously set playlist. Controller will get the whole set of
+ * playlist again.
+ */
+ public void notifyMetadataChanged() {
+ mProvider.notifyMetadataChanged_impl();
+ }
+
+ /**
+ * Send custom command to all connected controllers.
+ *
+ * @param command a command
+ * @param args optional argument
+ */
+ public void sendCustomCommand(@NonNull Command command, @Nullable Bundle args) {
+ mProvider.sendCustomCommand_impl(command, args);
+ }
+
+ /**
+ * Send custom command to a specific controller.
+ *
+ * @param command a command
+ * @param args optional argument
+ * @param receiver result receiver for the session
+ */
+ public void sendCustomCommand(@NonNull ControllerInfo controller, @NonNull Command command,
+ @Nullable Bundle args, @Nullable ResultReceiver receiver) {
+ // Equivalent to the MediaController.sendCustomCommand(Action action, ResultReceiver r);
+ mProvider.sendCustomCommand_impl(controller, command, args, receiver);
+ }
+
+ /**
+ * Play playback
+ */
+ public void play() {
+ mProvider.play_impl();
+ }
+
+ /**
+ * Pause playback
+ */
+ public void pause() {
+ mProvider.pause_impl();
+ }
+
+ /**
+ * Stop playback
+ */
+ public void stop() {
+ mProvider.stop_impl();
+ }
+
+ /**
+ * Rewind playback
+ */
+ public void skipToPrevious() {
+ mProvider.skipToPrevious_impl();
+ }
+
+ /**
+ * Rewind playback
+ */
+ public void skipToNext() {
+ mProvider.skipToNext_impl();
+ }
+
+ /**
+ * Request that the player prepare its playback. In other words, other sessions can continue
+ * to play during the preparation of this session. This method can be used to speed up the
+ * start of the playback. Once the preparation is done, the session will change its playback
+ * state to {@link PlaybackState#STATE_PAUSED}. Afterwards, {@link #play} can be called to
+ * start playback.
+ */
+ public void prepare() {
+ mProvider.prepare_impl();
+ }
+
+ /**
+ * Start fast forwarding. If playback is already fast forwarding this may increase the rate.
+ */
+ public void fastForward() {
+ mProvider.fastForward_impl();
+ }
+
+ /**
+ * Start rewinding. If playback is already rewinding this may increase the rate.
+ */
+ public void rewind() {
+ mProvider.rewind_impl();
+ }
+
+ /**
+ * Move to a new location in the media stream.
+ *
+ * @param pos Position to move to, in milliseconds.
+ */
+ public void seekTo(long pos) {
+ mProvider.seekTo_impl(pos);
+ }
+
+ /**
+ * Sets the index of current DataSourceDesc in the play list to be played.
+ *
+ * @param index the index of DataSourceDesc in the play list you want to play
+ * @throws IllegalArgumentException if the play list is null
+ * @throws NullPointerException if index is outside play list range
+ */
+ public void setCurrentPlaylistItem(int index) {
+ mProvider.setCurrentPlaylistItem_impl(index);
+ }
+
+ /**
+ * @hide
+ */
+ public void skipForward() {
+ // To match with KEYCODE_MEDIA_SKIP_FORWARD
+ }
+
+ /**
+ * @hide
+ */
+ public void skipBackward() {
+ // To match with KEYCODE_MEDIA_SKIP_BACKWARD
+ }
+
+ public void setPlaylist(@NonNull List<MediaItem2> playlist, @NonNull PlaylistParam param) {
+ mProvider.setPlaylist_impl(playlist, param);
+ }
+}
diff --git a/android/media/MediaSession2Test.java b/android/media/MediaSession2Test.java
new file mode 100644
index 0000000..045dcd5
--- /dev/null
+++ b/android/media/MediaSession2Test.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.MediaSession2.Builder;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.session.PlaybackState;
+import android.os.Process;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.util.ArrayList;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static android.media.TestUtils.createPlaybackState;
+import static org.junit.Assert.*;
+
+/**
+ * Tests {@link MediaSession2}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MediaSession2Test extends MediaSession2TestBase {
+ private static final String TAG = "MediaSession2Test";
+
+ private MediaSession2 mSession;
+ private MockPlayer mPlayer;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ sHandler.postAndSync(() -> {
+ mPlayer = new MockPlayer(0);
+ mSession = new MediaSession2.Builder(mContext, mPlayer).build();
+ });
+ }
+
+ @After
+ @Override
+ public void cleanUp() throws Exception {
+ super.cleanUp();
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ });
+ }
+
+ @Test
+ public void testBuilder() throws Exception {
+ try {
+ MediaSession2.Builder builder = new Builder(mContext, null);
+ fail("null player shouldn't be allowed");
+ } catch (IllegalArgumentException e) {
+ // expected. pass-through
+ }
+ MediaSession2.Builder builder = new Builder(mContext, mPlayer);
+ try {
+ builder.setId(null);
+ fail("null id shouldn't be allowed");
+ } catch (IllegalArgumentException e) {
+ // expected. pass-through
+ }
+ }
+
+ @Test
+ public void testSetPlayer() throws Exception {
+ sHandler.postAndSync(() -> {
+ MockPlayer player = new MockPlayer(0);
+ // Test if setPlayer doesn't crash with various situations.
+ mSession.setPlayer(mPlayer);
+ mSession.setPlayer(player);
+ mSession.close();
+ });
+ }
+
+ @Test
+ public void testPlay() throws Exception {
+ sHandler.postAndSync(() -> {
+ mSession.play();
+ assertTrue(mPlayer.mPlayCalled);
+ });
+ }
+
+ @Test
+ public void testPause() throws Exception {
+ sHandler.postAndSync(() -> {
+ mSession.pause();
+ assertTrue(mPlayer.mPauseCalled);
+ });
+ }
+
+ @Test
+ public void testStop() throws Exception {
+ sHandler.postAndSync(() -> {
+ mSession.stop();
+ assertTrue(mPlayer.mStopCalled);
+ });
+ }
+
+ @Test
+ public void testSkipToNext() throws Exception {
+ sHandler.postAndSync(() -> {
+ mSession.skipToNext();
+ assertTrue(mPlayer.mSkipToNextCalled);
+ });
+ }
+
+ @Test
+ public void testSkipToPrevious() throws Exception {
+ sHandler.postAndSync(() -> {
+ mSession.skipToPrevious();
+ assertTrue(mPlayer.mSkipToPreviousCalled);
+ });
+ }
+
+ @Test
+ public void testPlaybackStateChangedListener() throws InterruptedException {
+ // TODO(jaewan): Add equivalent tests again
+ /*
+ final CountDownLatch latch = new CountDownLatch(2);
+ final MockPlayer player = new MockPlayer(0);
+ final PlaybackListener listener = (state) -> {
+ assertEquals(sHandler.getLooper(), Looper.myLooper());
+ assertNotNull(state);
+ switch ((int) latch.getCount()) {
+ case 2:
+ assertEquals(PlaybackState.STATE_PLAYING, state.getState());
+ break;
+ case 1:
+ assertEquals(PlaybackState.STATE_PAUSED, state.getState());
+ break;
+ case 0:
+ fail();
+ }
+ latch.countDown();
+ };
+ player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
+ sHandler.postAndSync(() -> {
+ mSession.addPlaybackListener(listener, sHandler);
+ // When the player is set, listeners will be notified about the player's current state.
+ mSession.setPlayer(player);
+ });
+ player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ */
+ }
+
+ @Test
+ public void testBadPlayer() throws InterruptedException {
+ // TODO(jaewan): Add equivalent tests again
+ /*
+ final CountDownLatch latch = new CountDownLatch(3); // expected call + 1
+ final BadPlayer player = new BadPlayer(0);
+ sHandler.postAndSync(() -> {
+ mSession.addPlaybackListener((state) -> {
+ // This will be called for every setPlayer() calls, but no more.
+ assertNull(state);
+ latch.countDown();
+ }, sHandler);
+ mSession.setPlayer(player);
+ mSession.setPlayer(mPlayer);
+ });
+ player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
+ assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ */
+ }
+
+ private static class BadPlayer extends MockPlayer {
+ public BadPlayer(int count) {
+ super(count);
+ }
+
+ @Override
+ public void removePlaybackListener(@NonNull PlaybackListener listener) {
+ // No-op. This bad player will keep push notification to the listener that is previously
+ // registered by session.setPlayer().
+ }
+ }
+
+ @Test
+ public void testOnCommandCallback() throws InterruptedException {
+ final MockOnCommandCallback callback = new MockOnCommandCallback();
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ mPlayer = new MockPlayer(1);
+ mSession = new MediaSession2.Builder(mContext, mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback).build();
+ });
+ MediaController2 controller = createController(mSession.getToken());
+ controller.pause();
+ assertFalse(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mPlayer.mPauseCalled);
+ assertEquals(1, callback.commands.size());
+ assertEquals(MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE,
+ (long) callback.commands.get(0).getCommandCode());
+ controller.skipToNext();
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mPlayer.mSkipToNextCalled);
+ assertFalse(mPlayer.mPauseCalled);
+ assertEquals(2, callback.commands.size());
+ assertEquals(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM,
+ (long) callback.commands.get(1).getCommandCode());
+ }
+
+ @Test
+ public void testOnConnectCallback() throws InterruptedException {
+ final MockOnConnectCallback sessionCallback = new MockOnConnectCallback();
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext, mPlayer)
+ .setSessionCallback(sHandlerExecutor, sessionCallback).build();
+ });
+ MediaController2 controller =
+ createController(mSession.getToken(), false, null);
+ assertNotNull(controller);
+ waitForConnect(controller, false);
+ waitForDisconnect(controller, true);
+ }
+
+ public class MockOnConnectCallback extends SessionCallback {
+ @Override
+ public MediaSession2.CommandGroup onConnect(ControllerInfo controllerInfo) {
+ if (Process.myUid() != controllerInfo.getUid()) {
+ return null;
+ }
+ assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
+ assertEquals(Process.myUid(), controllerInfo.getUid());
+ assertFalse(controllerInfo.isTrusted());
+ // Reject all
+ return null;
+ }
+ }
+
+ public class MockOnCommandCallback extends SessionCallback {
+ public final ArrayList<MediaSession2.Command> commands = new ArrayList<>();
+
+ @Override
+ public boolean onCommandRequest(ControllerInfo controllerInfo, MediaSession2.Command command) {
+ assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
+ assertEquals(Process.myUid(), controllerInfo.getUid());
+ assertFalse(controllerInfo.isTrusted());
+ commands.add(command);
+ if (command.getCommandCode() == MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE) {
+ return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/android/media/MediaSession2TestBase.java b/android/media/MediaSession2TestBase.java
new file mode 100644
index 0000000..96afcb9
--- /dev/null
+++ b/android/media/MediaSession2TestBase.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.media.MediaController2.ControllerCallback;
+import android.media.MediaSession2.CommandGroup;
+import android.os.Bundle;
+import android.os.HandlerThread;
+import android.support.annotation.CallSuper;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.test.InstrumentationRegistry;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+/**
+ * Base class for session test.
+ */
+abstract class MediaSession2TestBase {
+ // Expected success
+ static final int WAIT_TIME_MS = 1000;
+
+ // Expected timeout
+ static final int TIMEOUT_MS = 500;
+
+ static TestUtils.SyncHandler sHandler;
+ static Executor sHandlerExecutor;
+
+ Context mContext;
+ private List<MediaController2> mControllers = new ArrayList<>();
+
+ interface TestControllerInterface {
+ ControllerCallback getCallback();
+ }
+
+ interface TestControllerCallbackInterface {
+ // Currently empty. Add methods in ControllerCallback/BrowserCallback that you want to test.
+
+ // Browser specific callbacks
+ default void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {}
+ }
+
+ interface WaitForConnectionInterface {
+ void waitForConnect(boolean expect) throws InterruptedException;
+ void waitForDisconnect(boolean expect) throws InterruptedException;
+ }
+
+ @BeforeClass
+ public static void setUpThread() {
+ if (sHandler == null) {
+ HandlerThread handlerThread = new HandlerThread("MediaSession2TestBase");
+ handlerThread.start();
+ sHandler = new TestUtils.SyncHandler(handlerThread.getLooper());
+ sHandlerExecutor = (runnable) -> {
+ sHandler.post(runnable);
+ };
+ }
+ }
+
+ @AfterClass
+ public static void cleanUpThread() {
+ if (sHandler != null) {
+ sHandler.getLooper().quitSafely();
+ sHandler = null;
+ sHandlerExecutor = null;
+ }
+ }
+
+ @CallSuper
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getTargetContext();
+ }
+
+ @CallSuper
+ public void cleanUp() throws Exception {
+ for (int i = 0; i < mControllers.size(); i++) {
+ mControllers.get(i).close();
+ }
+ }
+
+ final MediaController2 createController(SessionToken2 token) throws InterruptedException {
+ return createController(token, true, null);
+ }
+
+ final MediaController2 createController(@NonNull SessionToken2 token,
+ boolean waitForConnect, @Nullable TestControllerCallbackInterface callback)
+ throws InterruptedException {
+ TestControllerInterface instance = onCreateController(token, callback);
+ if (!(instance instanceof MediaController2)) {
+ throw new RuntimeException("Test has a bug. Expected MediaController2 but returned "
+ + instance);
+ }
+ MediaController2 controller = (MediaController2) instance;
+ mControllers.add(controller);
+ if (waitForConnect) {
+ waitForConnect(controller, true);
+ }
+ return controller;
+ }
+
+ private static WaitForConnectionInterface getWaitForConnectionInterface(
+ MediaController2 controller) {
+ if (!(controller instanceof TestControllerInterface)) {
+ throw new RuntimeException("Test has a bug. Expected controller implemented"
+ + " TestControllerInterface but got " + controller);
+ }
+ ControllerCallback callback = ((TestControllerInterface) controller).getCallback();
+ if (!(callback instanceof WaitForConnectionInterface)) {
+ throw new RuntimeException("Test has a bug. Expected controller with callback "
+ + " implemented WaitForConnectionInterface but got " + controller);
+ }
+ return (WaitForConnectionInterface) callback;
+ }
+
+ public static void waitForConnect(MediaController2 controller, boolean expected)
+ throws InterruptedException {
+ getWaitForConnectionInterface(controller).waitForConnect(expected);
+ }
+
+ public static void waitForDisconnect(MediaController2 controller, boolean expected)
+ throws InterruptedException {
+ getWaitForConnectionInterface(controller).waitForDisconnect(expected);
+ }
+
+ TestControllerInterface onCreateController(@NonNull SessionToken2 token,
+ @NonNull TestControllerCallbackInterface callback) {
+ return new TestMediaController(mContext, token, new TestControllerCallback(callback));
+ }
+
+ public static class TestControllerCallback extends MediaController2.ControllerCallback
+ implements WaitForConnectionInterface {
+ public final TestControllerCallbackInterface mCallbackProxy;
+ public final CountDownLatch connectLatch = new CountDownLatch(1);
+ public final CountDownLatch disconnectLatch = new CountDownLatch(1);
+
+ TestControllerCallback(TestControllerCallbackInterface callbackProxy) {
+ mCallbackProxy = callbackProxy;
+ }
+
+ @CallSuper
+ @Override
+ public void onConnected(CommandGroup commands) {
+ super.onConnected(commands);
+ connectLatch.countDown();
+ }
+
+ @CallSuper
+ @Override
+ public void onDisconnected() {
+ super.onDisconnected();
+ disconnectLatch.countDown();
+ }
+
+ @Override
+ public void waitForConnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Override
+ public void waitForDisconnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+ }
+
+ public class TestMediaController extends MediaController2 implements TestControllerInterface {
+ private final ControllerCallback mCallback;
+
+ public TestMediaController(@NonNull Context context, @NonNull SessionToken2 token,
+ @NonNull ControllerCallback callback) {
+ super(context, token, callback, sHandlerExecutor);
+ mCallback = callback;
+ }
+
+ @Override
+ public ControllerCallback getCallback() {
+ return mCallback;
+ }
+ }
+}
diff --git a/android/media/MediaSessionManager_MediaSession2.java b/android/media/MediaSessionManager_MediaSession2.java
new file mode 100644
index 0000000..192cbc2
--- /dev/null
+++ b/android/media/MediaSessionManager_MediaSession2.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.content.Context;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static android.media.TestUtils.createPlaybackState;
+import static org.junit.Assert.*;
+
+/**
+ * Tests {@link MediaSessionManager} with {@link MediaSession2} specific APIs.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Ignore
+// TODO(jaewan): Reenable test when the media session service detects newly installed sesison
+// service app.
+public class MediaSessionManager_MediaSession2 extends MediaSession2TestBase {
+ private static final String TAG = "MediaSessionManager_MediaSession2";
+
+ private MediaSessionManager mManager;
+ private MediaSession2 mSession;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mManager = (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
+
+ // Specify TAG here so {@link MediaSession2.getInstance()} doesn't complaint about
+ // per test thread differs across the {@link MediaSession2} with the same TAG.
+ final MockPlayer player = new MockPlayer(1);
+ sHandler.postAndSync(() -> {
+ mSession = new MediaSession2.Builder(mContext, player).setId(TAG).build();
+ });
+ ensureChangeInSession();
+ }
+
+ @After
+ @Override
+ public void cleanUp() throws Exception {
+ super.cleanUp();
+ sHandler.removeCallbacksAndMessages(null);
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ });
+ }
+
+ // TODO(jaewan): Make this host-side test to see per-user behavior.
+ @Test
+ public void testGetMediaSession2Tokens_hasMediaController() throws InterruptedException {
+ final MockPlayer player = (MockPlayer) mSession.getPlayer();
+ player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_STOPPED));
+
+ MediaController2 controller = null;
+ List<SessionToken2> tokens = mManager.getActiveSessionTokens();
+ assertNotNull(tokens);
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ if (mContext.getPackageName().equals(token.getPackageName())
+ && TAG.equals(token.getId())) {
+ assertNotNull(token.getSessionBinder());
+ assertNull(controller);
+ controller = createController(token);
+ }
+ }
+ assertNotNull(controller);
+
+ // Test if the found controller is correct one.
+ assertEquals(PlaybackState.STATE_STOPPED, controller.getPlaybackState().getState());
+ controller.play();
+
+ assertTrue(player.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(player.mPlayCalled);
+ }
+
+ /**
+ * Test if server recognizes session even if session refuses the connection from server.
+ *
+ * @throws InterruptedException
+ */
+ @Test
+ public void testGetSessionTokens_sessionRejected() throws InterruptedException {
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext, new MockPlayer(0)).setId(TAG)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
+ // Reject all connection request.
+ return null;
+ }
+ }).build();
+ });
+ ensureChangeInSession();
+
+ boolean foundSession = false;
+ List<SessionToken2> tokens = mManager.getActiveSessionTokens();
+ assertNotNull(tokens);
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ if (mContext.getPackageName().equals(token.getPackageName())
+ && TAG.equals(token.getId())) {
+ assertFalse(foundSession);
+ foundSession = true;
+ }
+ }
+ assertTrue(foundSession);
+ }
+
+ @Test
+ public void testGetMediaSession2Tokens_playerRemoved() throws InterruptedException {
+ // Release
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ });
+ ensureChangeInSession();
+
+ // When the mSession's player becomes null, it should lose binder connection between server.
+ // So server will forget the session.
+ List<SessionToken2> tokens = mManager.getActiveSessionTokens();
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ assertFalse(mContext.getPackageName().equals(token.getPackageName())
+ && TAG.equals(token.getId()));
+ }
+ }
+
+ @Test
+ public void testGetMediaSessionService2Token() throws InterruptedException {
+ boolean foundTestSessionService = false;
+ boolean foundTestLibraryService = false;
+ List<SessionToken2> tokens = mManager.getSessionServiceTokens();
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ if (mContext.getPackageName().equals(token.getPackageName())
+ && MockMediaSessionService2.ID.equals(token.getId())) {
+ assertFalse(foundTestSessionService);
+ assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+ assertNull(token.getSessionBinder());
+ foundTestSessionService = true;
+ } else if (mContext.getPackageName().equals(token.getPackageName())
+ && MockMediaLibraryService2.ID.equals(token.getId())) {
+ assertFalse(foundTestLibraryService);
+ assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
+ assertNull(token.getSessionBinder());
+ foundTestLibraryService = true;
+ }
+ }
+ assertTrue(foundTestSessionService);
+ assertTrue(foundTestLibraryService);
+ }
+
+ @Test
+ public void testGetAllSessionTokens() throws InterruptedException {
+ boolean foundTestSession = false;
+ boolean foundTestSessionService = false;
+ boolean foundTestLibraryService = false;
+ List<SessionToken2> tokens = mManager.getAllSessionTokens();
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ if (!mContext.getPackageName().equals(token.getPackageName())) {
+ continue;
+ }
+ switch (token.getId()) {
+ case TAG:
+ assertFalse(foundTestSession);
+ foundTestSession = true;
+ break;
+ case MockMediaSessionService2.ID:
+ assertFalse(foundTestSessionService);
+ foundTestSessionService = true;
+ assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+ break;
+ case MockMediaLibraryService2.ID:
+ assertFalse(foundTestLibraryService);
+ assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
+ foundTestLibraryService = true;
+ break;
+ default:
+ fail("Unexpected session " + token + " exists in the package");
+ }
+ }
+ assertTrue(foundTestSession);
+ assertTrue(foundTestSessionService);
+ assertTrue(foundTestLibraryService);
+ }
+
+ // Ensures if the session creation/release is notified to the server.
+ private void ensureChangeInSession() throws InterruptedException {
+ // TODO(jaewan): Wait by listener.
+ Thread.sleep(WAIT_TIME_MS);
+ }
+}
diff --git a/android/media/MediaSessionService2.java b/android/media/MediaSessionService2.java
new file mode 100644
index 0000000..19814f0
--- /dev/null
+++ b/android/media/MediaSessionService2.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.Service;
+import android.content.Intent;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.session.PlaybackState;
+import android.media.update.ApiLoader;
+import android.media.update.MediaSessionService2Provider;
+import android.os.IBinder;
+
+/**
+ * Base class for media session services, which is the service version of the {@link MediaSession2}.
+ * <p>
+ * It's highly recommended for an app to use this instead of {@link MediaSession2} if it wants
+ * to keep media playback in the background.
+ * <p>
+ * Here's the benefits of using {@link MediaSessionService2} instead of
+ * {@link MediaSession2}.
+ * <ul>
+ * <li>Another app can know that your app supports {@link MediaSession2} even when your app
+ * isn't running.
+ * <li>Another app can start playback of your app even when your app isn't running.
+ * </ul>
+ * For example, user's voice command can start playback of your app even when it's not running.
+ * <p>
+ * To extend this class, adding followings directly to your {@code AndroidManifest.xml}.
+ * <pre>
+ * <service android:name="component_name_of_your_implementation" >
+ * <intent-filter>
+ * <action android:name="android.media.MediaSessionService2" />
+ * </intent-filter>
+ * </service></pre>
+ * <p>
+ * A {@link MediaSessionService2} is another form of {@link MediaSession2}. IDs shouldn't
+ * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By
+ * default, an empty string will be used for ID of the service. If you want to specify an ID,
+ * declare metadata in the manifest as follows.
+ * <pre>
+ * <service android:name="component_name_of_your_implementation" >
+ * <intent-filter>
+ * <action android:name="android.media.MediaSessionService2" />
+ * </intent-filter>
+ * <meta-data android:name="android.media.session"
+ * android:value="session_id"/>
+ * </service></pre>
+ * <p>
+ * It's recommended for an app to have a single {@link MediaSessionService2} declared in the
+ * manifest. Otherwise, your app might be shown twice in the list of the Auto/Wearable, or another
+ * app fails to pick the right session service when it wants to start the playback this app.
+ * <p>
+ * If there's conflicts with the session ID among the services, services wouldn't be available for
+ * any controllers.
+ * <p>
+ * Topic covered here:
+ * <ol>
+ * <li><a href="#ServiceLifecycle">Service Lifecycle</a>
+ * <li><a href="#Permissions">Permissions</a>
+ * </ol>
+ * <div class="special reference">
+ * <a name="ServiceLifecycle"></a>
+ * <h3>Service Lifecycle</h3>
+ * <p>
+ * Session service is bounded service. When a {@link MediaController2} is created for the
+ * session service, the controller binds to the session service. {@link #onCreateSession(String)}
+ * may be called after the {@link #onCreate} if the service hasn't created yet.
+ * <p>
+ * After the binding, session's {@link MediaSession2.SessionCallback#onConnect(ControllerInfo)}
+ * will be called to accept or reject connection request from a controller. If the connection is
+ * rejected, the controller will unbind. If it's accepted, the controller will be available to use
+ * and keep binding.
+ * <p>
+ * When playback is started for this session service, {@link #onUpdateNotification(PlaybackState)}
+ * is called and service would become a foreground service. It's needed to keep playback after the
+ * controller is destroyed. The session service becomes background service when the playback is
+ * stopped.
+ * <a name="Permissions"></a>
+ * <h3>Permissions</h3>
+ * <p>
+ * Any app can bind to the session service with controller, but the controller can be used only if
+ * the session service accepted the connection request through
+ * {@link MediaSession2.SessionCallback#onConnect(ControllerInfo)}.
+ *
+ * @hide
+ */
+// TODO(jaewan): Unhide
+// TODO(jaewan): Can we clean up sessions in onDestroy() automatically instead?
+// What about currently running SessionCallback when the onDestroy() is called?
+// TODO(jaewan): Protect this with system|privilleged permission - Q.
+// TODO(jaewan): Add permission check for the service to know incoming connection request.
+// Follow-up questions: What about asking a XML for list of white/black packages for
+// allowing enumeration?
+// We can read the information even when the service is started,
+// so SessionManager.getXXXXService() can only return apps
+// TODO(jaewan): Will be the black/white listing persistent?
+// In other words, can we cache the rejection?
+public abstract class MediaSessionService2 extends Service {
+ private final MediaSessionService2Provider mProvider;
+
+ /**
+ * This is the interface name that a service implementing a session service should say that it
+ * support -- that is, this is the action it uses for its intent filter.
+ */
+ public static final String SERVICE_INTERFACE = "android.media.MediaSessionService2";
+
+ /**
+ * Name under which a MediaSessionService2 component publishes information about itself.
+ * This meta-data must provide a string value for the ID.
+ */
+ public static final String SERVICE_META_DATA = "android.media.session";
+
+ public MediaSessionService2() {
+ super();
+ mProvider = createProvider();
+ }
+
+ MediaSessionService2Provider createProvider() {
+ return ApiLoader.getProvider(this).createMediaSessionService2(this);
+ }
+
+ /**
+ * Default implementation for {@link MediaSessionService2} to initialize session service.
+ * <p>
+ * Override this method if you need your own initialization. Derived classes MUST call through
+ * to the super class's implementation of this method.
+ */
+ @CallSuper
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mProvider.onCreate_impl();
+ }
+
+ /**
+ * Called when another app requested to start this service to get {@link MediaSession2}.
+ * <p>
+ * Session service will accept or reject the connection with the
+ * {@link MediaSession2.SessionCallback} in the created session.
+ * <p>
+ * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the
+ * expected ID that you've specified through the AndroidManifest.xml.
+ * <p>
+ * This method will be called on the main thread.
+ *
+ * @param sessionId session id written in the AndroidManifest.xml.
+ * @return a new session
+ * @see MediaSession2.Builder
+ * @see #getSession()
+ */
+ public @NonNull abstract MediaSession2 onCreateSession(String sessionId);
+
+ /**
+ * Called when the playback state of this session is changed, and notification needs update.
+ * Override this method to show your own notification UI.
+ * <p>
+ * With the notification returned here, the service become foreground service when the playback
+ * is started. It becomes background service after the playback is stopped.
+ *
+ * @param state playback state
+ * @return a {@link MediaNotification}. If it's {@code null}, notification wouldn't be shown.
+ */
+ // TODO(jaewan): Also add metadata
+ public MediaNotification onUpdateNotification(PlaybackState2 state) {
+ return mProvider.onUpdateNotification_impl(state);
+ }
+
+ /**
+ * Get instance of the {@link MediaSession2} that you've previously created with the
+ * {@link #onCreateSession} for this service.
+ *
+ * @return created session
+ */
+ public final MediaSession2 getSession() {
+ return mProvider.getSession_impl();
+ }
+
+ /**
+ * Default implementation for {@link MediaSessionService2} to handle incoming binding
+ * request. If the request is for getting the session, the intent will have action
+ * {@link #SERVICE_INTERFACE}.
+ * <p>
+ * Override this method if this service also needs to handle binder requests other than
+ * {@link #SERVICE_INTERFACE}. Derived classes MUST call through to the super class's
+ * implementation of this method.
+ *
+ * @param intent
+ * @return Binder
+ */
+ @CallSuper
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mProvider.onBind_impl(intent);
+ }
+
+ /**
+ * Returned by {@link #onUpdateNotification(PlaybackState)} for making session service
+ * foreground service to keep playback running in the background. It's highly recommended to
+ * show media style notification here.
+ */
+ // TODO(jaewan): Should we also move this to updatable?
+ public static class MediaNotification {
+ public final int id;
+ public final Notification notification;
+
+ private MediaNotification(int id, @NonNull Notification notification) {
+ this.id = id;
+ this.notification = notification;
+ }
+
+ /**
+ * Create a {@link MediaNotification}.
+ *
+ * @param notificationId notification id to be used for
+ * {@link android.app.NotificationManager#notify(int, Notification)}.
+ * @param notification a notification to make session service foreground service. Media
+ * style notification is recommended here.
+ * @return
+ */
+ public static MediaNotification create(int notificationId,
+ @NonNull Notification notification) {
+ if (notification == null) {
+ throw new IllegalArgumentException("Notification cannot be null");
+ }
+ return new MediaNotification(notificationId, notification);
+ }
+ }
+}
diff --git a/android/media/MockMediaLibraryService2.java b/android/media/MockMediaLibraryService2.java
new file mode 100644
index 0000000..14cf257
--- /dev/null
+++ b/android/media/MockMediaLibraryService2.java
@@ -0,0 +1,98 @@
+/*
+* Copyright 2018 The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package android.media;
+
+import static junit.framework.Assert.fail;
+
+import android.content.Context;
+import android.media.MediaSession2.CommandGroup;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.TestUtils.SyncHandler;
+import android.os.Bundle;
+import android.os.Process;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Mock implementation of {@link MediaLibraryService2} for testing.
+ */
+public class MockMediaLibraryService2 extends MediaLibraryService2 {
+ // Keep in sync with the AndroidManifest.xml
+ public static final String ID = "TestLibrary";
+
+ public static final String ROOT_ID = "rootId";
+ public static final Bundle EXTRA = new Bundle();
+ static {
+ EXTRA.putString(ROOT_ID, ROOT_ID);
+ }
+ @GuardedBy("MockMediaLibraryService2.class")
+ private static SessionToken2 sToken;
+
+ private MediaLibrarySession mSession;
+
+ @Override
+ public MediaLibrarySession onCreateSession(String sessionId) {
+ final MockPlayer player = new MockPlayer(1);
+ final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
+ try {
+ handler.postAndSync(() -> {
+ TestLibrarySessionCallback callback = new TestLibrarySessionCallback();
+ mSession = new MediaLibrarySessionBuilder(MockMediaLibraryService2.this,
+ player, (runnable) -> handler.post(runnable), callback)
+ .setId(sessionId).build();
+ });
+ } catch (InterruptedException e) {
+ fail(e.toString());
+ }
+ return mSession;
+ }
+
+ @Override
+ public void onDestroy() {
+ TestServiceRegistry.getInstance().cleanUp();
+ super.onDestroy();
+ }
+
+ public static SessionToken2 getToken(Context context) {
+ synchronized (MockMediaLibraryService2.class) {
+ if (sToken == null) {
+ sToken = new SessionToken2(SessionToken2.TYPE_LIBRARY_SERVICE,
+ context.getPackageName(), ID,
+ MockMediaLibraryService2.class.getName(), null);
+ }
+ return sToken;
+ }
+ }
+
+ private class TestLibrarySessionCallback extends MediaLibrarySessionCallback {
+ @Override
+ public CommandGroup onConnect(ControllerInfo controller) {
+ if (Process.myUid() != controller.getUid()) {
+ // It's system app wants to listen changes. Ignore.
+ return super.onConnect(controller);
+ }
+ TestServiceRegistry.getInstance().setServiceInstance(
+ MockMediaLibraryService2.this, controller);
+ return super.onConnect(controller);
+ }
+
+ @Override
+ public BrowserRoot onGetRoot(ControllerInfo controller, Bundle rootHints) {
+ return new BrowserRoot(ROOT_ID, EXTRA);
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/media/MockMediaSessionService2.java b/android/media/MockMediaSessionService2.java
new file mode 100644
index 0000000..b058117
--- /dev/null
+++ b/android/media/MockMediaSessionService2.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import static junit.framework.Assert.fail;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.TestUtils.SyncHandler;
+import android.media.session.PlaybackState;
+import android.os.Process;
+
+/**
+ * Mock implementation of {@link android.media.MediaSessionService2} for testing.
+ */
+public class MockMediaSessionService2 extends MediaSessionService2 {
+ // Keep in sync with the AndroidManifest.xml
+ public static final String ID = "TestSession";
+
+ private static final String DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID = "media_session_service";
+ private static final int DEFAULT_MEDIA_NOTIFICATION_ID = 1001;
+
+ private NotificationChannel mDefaultNotificationChannel;
+ private MediaSession2 mSession;
+ private NotificationManager mNotificationManager;
+
+ @Override
+ public MediaSession2 onCreateSession(String sessionId) {
+ final MockPlayer player = new MockPlayer(1);
+ final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
+ try {
+ handler.postAndSync(() -> {
+ mSession = new MediaSession2.Builder(MockMediaSessionService2.this, player)
+ .setId(sessionId).setSessionCallback((runnable)->handler.post(runnable),
+ new MySessionCallback()).build();
+ });
+ } catch (InterruptedException e) {
+ fail(e.toString());
+ }
+ return mSession;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ @Override
+ public void onDestroy() {
+ TestServiceRegistry.getInstance().cleanUp();
+ super.onDestroy();
+ }
+
+ @Override
+ public MediaNotification onUpdateNotification(PlaybackState2 state) {
+ if (mDefaultNotificationChannel == null) {
+ mDefaultNotificationChannel = new NotificationChannel(
+ DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
+ DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
+ NotificationManager.IMPORTANCE_DEFAULT);
+ mNotificationManager.createNotificationChannel(mDefaultNotificationChannel);
+ }
+ Notification notification = new Notification.Builder(
+ this, DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID)
+ .setContentTitle(getPackageName())
+ .setContentText("Playback state: " + state.getState())
+ .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
+ return MediaNotification.create(DEFAULT_MEDIA_NOTIFICATION_ID, notification);
+ }
+
+ private class MySessionCallback extends SessionCallback {
+ @Override
+ public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
+ if (Process.myUid() != controller.getUid()) {
+ // It's system app wants to listen changes. Ignore.
+ return super.onConnect(controller);
+ }
+ TestServiceRegistry.getInstance().setServiceInstance(
+ MockMediaSessionService2.this, controller);
+ return super.onConnect(controller);
+ }
+ }
+}
diff --git a/android/media/MockPlayer.java b/android/media/MockPlayer.java
new file mode 100644
index 0000000..fd69309
--- /dev/null
+++ b/android/media/MockPlayer.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.media.MediaSession2.PlaylistParam;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+
+/**
+ * A mock implementation of {@link MediaPlayerBase} for testing.
+ */
+public class MockPlayer extends MediaPlayerBase {
+ public final CountDownLatch mCountDownLatch;
+
+ public boolean mPlayCalled;
+ public boolean mPauseCalled;
+ public boolean mStopCalled;
+ public boolean mSkipToPreviousCalled;
+ public boolean mSkipToNextCalled;
+ public List<PlaybackListenerHolder> mListeners = new ArrayList<>();
+ private PlaybackState2 mLastPlaybackState;
+
+ public MockPlayer(int count) {
+ mCountDownLatch = (count > 0) ? new CountDownLatch(count) : null;
+ }
+
+ @Override
+ public void play() {
+ mPlayCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void pause() {
+ mPauseCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void stop() {
+ mStopCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void skipToPrevious() {
+ mSkipToPreviousCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void skipToNext() {
+ mSkipToNextCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+
+
+ @Nullable
+ @Override
+ public PlaybackState2 getPlaybackState() {
+ return mLastPlaybackState;
+ }
+
+ @Override
+ public void addPlaybackListener(@NonNull Executor executor,
+ @NonNull PlaybackListener listener) {
+ mListeners.add(new PlaybackListenerHolder(executor, listener));
+ }
+
+ @Override
+ public void removePlaybackListener(@NonNull PlaybackListener listener) {
+ int index = PlaybackListenerHolder.indexOf(mListeners, listener);
+ if (index >= 0) {
+ mListeners.remove(index);
+ }
+ }
+
+ public void notifyPlaybackState(final PlaybackState2 state) {
+ mLastPlaybackState = state;
+ for (int i = 0; i < mListeners.size(); i++) {
+ mListeners.get(i).postPlaybackChange(state);
+ }
+ }
+
+ // No-op. Should be added for test later.
+ @Override
+ public void prepare() {
+ }
+
+ @Override
+ public void seekTo(long pos) {
+ }
+
+ @Override
+ public void fastFoward() {
+ }
+
+ @Override
+ public void rewind() {
+ }
+
+ @Override
+ public AudioAttributes getAudioAttributes() {
+ return null;
+ }
+
+ @Override
+ public void setPlaylist(List<MediaItem2> item, PlaylistParam param) {
+ }
+
+ @Override
+ public void setCurrentPlaylistItem(int index) {
+ }
+}
diff --git a/android/media/PlaybackListenerHolder.java b/android/media/PlaybackListenerHolder.java
new file mode 100644
index 0000000..4e19d4d
--- /dev/null
+++ b/android/media/PlaybackListenerHolder.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.os.Message;
+import android.support.annotation.NonNull;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Holds {@link PlaybackListener} with the {@link Handler}.
+ */
+public class PlaybackListenerHolder {
+ public final Executor executor;
+ public final PlaybackListener listener;
+
+ public PlaybackListenerHolder(Executor executor, @NonNull PlaybackListener listener) {
+ this.executor = executor;
+ this.listener = listener;
+ }
+
+ public void postPlaybackChange(final PlaybackState2 state) {
+ executor.execute(() -> listener.onPlaybackChanged(state));
+ }
+
+ /**
+ * Returns {@code true} if the given list contains a {@link PlaybackListenerHolder} that holds
+ * the given listener.
+ *
+ * @param list list to check
+ * @param listener listener to check
+ * @return {@code true} if the given list contains listener. {@code false} otherwise.
+ */
+ public static <Holder extends PlaybackListenerHolder> boolean contains(
+ @NonNull List<Holder> list, PlaybackListener listener) {
+ return indexOf(list, listener) >= 0;
+ }
+
+ /**
+ * Returns the index of the {@link PlaybackListenerHolder} that contains the given listener.
+ *
+ * @param list list to check
+ * @param listener listener to check
+ * @return {@code index} of item if the given list contains listener. {@code -1} otherwise.
+ */
+ public static <Holder extends PlaybackListenerHolder> int indexOf(
+ @NonNull List<Holder> list, PlaybackListener listener) {
+ for (int i = 0; i < list.size(); i++) {
+ if (list.get(i).listener == listener) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/android/media/PlaybackState2.java b/android/media/PlaybackState2.java
new file mode 100644
index 0000000..46d6f45
--- /dev/null
+++ b/android/media/PlaybackState2.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.annotation.IntDef;
+import android.os.Bundle;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Playback state for a {@link MediaPlayerBase}, to be shared between {@link MediaSession2} and
+ * {@link MediaController2}. This includes a playback state {@link #STATE_PLAYING},
+ * the current playback position and extra.
+ * @hide
+ */
+// TODO(jaewan): Move to updatable
+public final class PlaybackState2 {
+ private static final String TAG = "PlaybackState2";
+
+ private static final String KEY_STATE = "android.media.playbackstate2.state";
+
+ // TODO(jaewan): Replace states from MediaPlayer2
+ /**
+ * @hide
+ */
+ @IntDef({STATE_NONE, STATE_STOPPED, STATE_PREPARED, STATE_PAUSED, STATE_PLAYING,
+ STATE_FINISH, STATE_BUFFERING, STATE_ERROR})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface State {}
+
+ /**
+ * This is the default playback state and indicates that no media has been
+ * added yet, or the performer has been reset and has no content to play.
+ */
+ public final static int STATE_NONE = 0;
+
+ /**
+ * State indicating this item is currently stopped.
+ */
+ public final static int STATE_STOPPED = 1;
+
+ /**
+ * State indicating this item is currently prepared
+ */
+ public final static int STATE_PREPARED = 2;
+
+ /**
+ * State indicating this item is currently paused.
+ */
+ public final static int STATE_PAUSED = 3;
+
+ /**
+ * State indicating this item is currently playing.
+ */
+ public final static int STATE_PLAYING = 4;
+
+ /**
+ * State indicating the playback reaches the end of the item.
+ */
+ public final static int STATE_FINISH = 5;
+
+ /**
+ * State indicating this item is currently buffering and will begin playing
+ * when enough data has buffered.
+ */
+ public final static int STATE_BUFFERING = 6;
+
+ /**
+ * State indicating this item is currently in an error state. The error
+ * message should also be set when entering this state.
+ */
+ public final static int STATE_ERROR = 7;
+
+ /**
+ * Use this value for the position to indicate the position is not known.
+ */
+ public final static long PLAYBACK_POSITION_UNKNOWN = -1;
+
+ private final int mState;
+ private final long mPosition;
+ private final long mBufferedPosition;
+ private final float mSpeed;
+ private final CharSequence mErrorMessage;
+ private final long mUpdateTime;
+ private final long mActiveItemId;
+
+ public PlaybackState2(int state, long position, long updateTime, float speed,
+ long bufferedPosition, long activeItemId, CharSequence error) {
+ mState = state;
+ mPosition = position;
+ mSpeed = speed;
+ mUpdateTime = updateTime;
+ mBufferedPosition = bufferedPosition;
+ mActiveItemId = activeItemId;
+ mErrorMessage = error;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder bob = new StringBuilder("PlaybackState {");
+ bob.append("state=").append(mState);
+ bob.append(", position=").append(mPosition);
+ bob.append(", buffered position=").append(mBufferedPosition);
+ bob.append(", speed=").append(mSpeed);
+ bob.append(", updated=").append(mUpdateTime);
+ bob.append(", active item id=").append(mActiveItemId);
+ bob.append(", error=").append(mErrorMessage);
+ bob.append("}");
+ return bob.toString();
+ }
+
+ /**
+ * Get the current state of playback. One of the following:
+ * <ul>
+ * <li> {@link PlaybackState2#STATE_NONE}</li>
+ * <li> {@link PlaybackState2#STATE_STOPPED}</li>
+ * <li> {@link PlaybackState2#STATE_PLAYING}</li>
+ * <li> {@link PlaybackState2#STATE_PAUSED}</li>
+ * <li> {@link PlaybackState2#STATE_BUFFERING}</li>
+ * <li> {@link PlaybackState2#STATE_ERROR}</li>
+ * </ul>
+ */
+ @State
+ public int getState() {
+ return mState;
+ }
+
+ /**
+ * Get the current playback position in ms.
+ */
+ public long getPosition() {
+ return mPosition;
+ }
+
+ /**
+ * Get the current buffered position in ms. This is the farthest playback
+ * point that can be reached from the current position using only buffered
+ * content.
+ */
+ public long getBufferedPosition() {
+ return mBufferedPosition;
+ }
+
+ /**
+ * Get the current playback speed as a multiple of normal playback. This
+ * should be negative when rewinding. A value of 1 means normal playback and
+ * 0 means paused.
+ *
+ * @return The current speed of playback.
+ */
+ public float getPlaybackSpeed() {
+ return mSpeed;
+ }
+
+ /**
+ * Get a user readable error message. This should be set when the state is
+ * {@link PlaybackState2#STATE_ERROR}.
+ */
+ public CharSequence getErrorMessage() {
+ return mErrorMessage;
+ }
+
+ /**
+ * Get the elapsed real time at which position was last updated. If the
+ * position has never been set this will return 0;
+ *
+ * @return The last time the position was updated.
+ */
+ public long getLastPositionUpdateTime() {
+ return mUpdateTime;
+ }
+
+ /**
+ * Get the id of the currently active item in the playlist.
+ *
+ * @return The id of the currently active item in the queue
+ */
+ public long getCurrentPlaylistItemIndex() {
+ return mActiveItemId;
+ }
+
+ /**
+ * @return Bundle object for this to share between processes.
+ */
+ public Bundle toBundle() {
+ // TODO(jaewan): Include other variables.
+ Bundle bundle = new Bundle();
+ bundle.putInt(KEY_STATE, mState);
+ return bundle;
+ }
+
+ /**
+ * @param bundle input
+ * @return
+ */
+ public static PlaybackState2 fromBundle(Bundle bundle) {
+ // TODO(jaewan): Include other variables.
+ final int state = bundle.getInt(KEY_STATE);
+ return new PlaybackState2(state, 0, 0, 0, 0, 0, null);
+ }
+}
\ No newline at end of file
diff --git a/android/media/Rating2.java b/android/media/Rating2.java
new file mode 100644
index 0000000..67e5e72
--- /dev/null
+++ b/android/media/Rating2.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.annotation.IntDef;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A class to encapsulate rating information used as content metadata.
+ * A rating is defined by its rating style (see {@link #RATING_HEART},
+ * {@link #RATING_THUMB_UP_DOWN}, {@link #RATING_3_STARS}, {@link #RATING_4_STARS},
+ * {@link #RATING_5_STARS} or {@link #RATING_PERCENTAGE}) and the actual rating value (which may
+ * be defined as "unrated"), both of which are defined when the rating instance is constructed
+ * through one of the factory methods.
+ * @hide
+ */
+// TODO(jaewan): Move this to updatable
+public final class Rating2 {
+ private static final String TAG = "Rating2";
+
+ private static final String KEY_STYLE = "android.media.rating2.style";
+ private static final String KEY_VALUE = "android.media.rating2.value";
+
+ /**
+ * @hide
+ */
+ @IntDef({RATING_NONE, RATING_HEART, RATING_THUMB_UP_DOWN, RATING_3_STARS, RATING_4_STARS,
+ RATING_5_STARS, RATING_PERCENTAGE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Style {}
+
+ /**
+ * @hide
+ */
+ @IntDef({RATING_3_STARS, RATING_4_STARS, RATING_5_STARS})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StarStyle {}
+
+ /**
+ * Indicates a rating style is not supported. A Rating2 will never have this
+ * type, but can be used by other classes to indicate they do not support
+ * Rating2.
+ */
+ public final static int RATING_NONE = 0;
+
+ /**
+ * A rating style with a single degree of rating, "heart" vs "no heart". Can be used to
+ * indicate the content referred to is a favorite (or not).
+ */
+ public final static int RATING_HEART = 1;
+
+ /**
+ * A rating style for "thumb up" vs "thumb down".
+ */
+ public final static int RATING_THUMB_UP_DOWN = 2;
+
+ /**
+ * A rating style with 0 to 3 stars.
+ */
+ public final static int RATING_3_STARS = 3;
+
+ /**
+ * A rating style with 0 to 4 stars.
+ */
+ public final static int RATING_4_STARS = 4;
+
+ /**
+ * A rating style with 0 to 5 stars.
+ */
+ public final static int RATING_5_STARS = 5;
+
+ /**
+ * A rating style expressed as a percentage.
+ */
+ public final static int RATING_PERCENTAGE = 6;
+
+ private final static float RATING_NOT_RATED = -1.0f;
+
+ private final int mRatingStyle;
+
+ private final float mRatingValue;
+
+ private Rating2(@Style int ratingStyle, float rating) {
+ mRatingStyle = ratingStyle;
+ mRatingValue = rating;
+ }
+
+ @Override
+ public String toString() {
+ return "Rating2:style=" + mRatingStyle + " rating="
+ + (mRatingValue < 0.0f ? "unrated" : String.valueOf(mRatingValue));
+ }
+
+ /**
+ * Create an instance from bundle object, previoulsy created by {@link #toBundle()}
+ *
+ * @param bundle bundle
+ * @return new Rating2 instance
+ */
+ public static Rating2 fromBundle(Bundle bundle) {
+ return new Rating2(bundle.getInt(KEY_STYLE), bundle.getFloat(KEY_VALUE));
+ }
+
+ /**
+ * Return bundle for this object to share across the process.
+ * @return bundle of this object
+ */
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(KEY_STYLE, mRatingStyle);
+ bundle.putFloat(KEY_VALUE, mRatingValue);
+ return bundle;
+ }
+
+ /**
+ * Return a Rating2 instance with no rating.
+ * Create and return a new Rating2 instance with no rating known for the given
+ * rating style.
+ * @param ratingStyle one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN},
+ * {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS},
+ * or {@link #RATING_PERCENTAGE}.
+ * @return null if an invalid rating style is passed, a new Rating2 instance otherwise.
+ */
+ public static Rating2 newUnratedRating(@Style int ratingStyle) {
+ switch(ratingStyle) {
+ case RATING_HEART:
+ case RATING_THUMB_UP_DOWN:
+ case RATING_3_STARS:
+ case RATING_4_STARS:
+ case RATING_5_STARS:
+ case RATING_PERCENTAGE:
+ return new Rating2(ratingStyle, RATING_NOT_RATED);
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Return a Rating2 instance with a heart-based rating.
+ * Create and return a new Rating2 instance with a rating style of {@link #RATING_HEART},
+ * and a heart-based rating.
+ * @param hasHeart true for a "heart selected" rating, false for "heart unselected".
+ * @return a new Rating2 instance.
+ */
+ public static Rating2 newHeartRating(boolean hasHeart) {
+ return new Rating2(RATING_HEART, hasHeart ? 1.0f : 0.0f);
+ }
+
+ /**
+ * Return a Rating2 instance with a thumb-based rating.
+ * Create and return a new Rating2 instance with a {@link #RATING_THUMB_UP_DOWN}
+ * rating style, and a "thumb up" or "thumb down" rating.
+ * @param thumbIsUp true for a "thumb up" rating, false for "thumb down".
+ * @return a new Rating2 instance.
+ */
+ public static Rating2 newThumbRating(boolean thumbIsUp) {
+ return new Rating2(RATING_THUMB_UP_DOWN, thumbIsUp ? 1.0f : 0.0f);
+ }
+
+ /**
+ * Return a Rating2 instance with a star-based rating.
+ * Create and return a new Rating2 instance with one of the star-base rating styles
+ * and the given integer or fractional number of stars. Non integer values can for instance
+ * be used to represent an average rating value, which might not be an integer number of stars.
+ * @param starRatingStyle one of {@link #RATING_3_STARS}, {@link #RATING_4_STARS},
+ * {@link #RATING_5_STARS}.
+ * @param starRating a number ranging from 0.0f to 3.0f, 4.0f or 5.0f according to
+ * the rating style.
+ * @return null if the rating style is invalid, or the rating is out of range,
+ * a new Rating2 instance otherwise.
+ */
+ public static Rating2 newStarRating(@StarStyle int starRatingStyle, float starRating) {
+ float maxRating = -1.0f;
+ switch(starRatingStyle) {
+ case RATING_3_STARS:
+ maxRating = 3.0f;
+ break;
+ case RATING_4_STARS:
+ maxRating = 4.0f;
+ break;
+ case RATING_5_STARS:
+ maxRating = 5.0f;
+ break;
+ default:
+ Log.e(TAG, "Invalid rating style (" + starRatingStyle + ") for a star rating");
+ return null;
+ }
+ if ((starRating < 0.0f) || (starRating > maxRating)) {
+ Log.e(TAG, "Trying to set out of range star-based rating");
+ return null;
+ }
+ return new Rating2(starRatingStyle, starRating);
+ }
+
+ /**
+ * Return a Rating2 instance with a percentage-based rating.
+ * Create and return a new Rating2 instance with a {@link #RATING_PERCENTAGE}
+ * rating style, and a rating of the given percentage.
+ * @param percent the value of the rating
+ * @return null if the rating is out of range, a new Rating2 instance otherwise.
+ */
+ public static Rating2 newPercentageRating(float percent) {
+ if ((percent < 0.0f) || (percent > 100.0f)) {
+ Log.e(TAG, "Invalid percentage-based rating value");
+ return null;
+ } else {
+ return new Rating2(RATING_PERCENTAGE, percent);
+ }
+ }
+
+ /**
+ * Return whether there is a rating value available.
+ * @return true if the instance was not created with {@link #newUnratedRating(int)}.
+ */
+ public boolean isRated() {
+ return mRatingValue >= 0.0f;
+ }
+
+ /**
+ * Return the rating style.
+ * @return one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN},
+ * {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS},
+ * or {@link #RATING_PERCENTAGE}.
+ */
+ @Style
+ public int getRatingStyle() {
+ return mRatingStyle;
+ }
+
+ /**
+ * Return whether the rating is "heart selected".
+ * @return true if the rating is "heart selected", false if the rating is "heart unselected",
+ * if the rating style is not {@link #RATING_HEART} or if it is unrated.
+ */
+ public boolean hasHeart() {
+ if (mRatingStyle != RATING_HEART) {
+ return false;
+ } else {
+ return (mRatingValue == 1.0f);
+ }
+ }
+
+ /**
+ * Return whether the rating is "thumb up".
+ * @return true if the rating is "thumb up", false if the rating is "thumb down",
+ * if the rating style is not {@link #RATING_THUMB_UP_DOWN} or if it is unrated.
+ */
+ public boolean isThumbUp() {
+ if (mRatingStyle != RATING_THUMB_UP_DOWN) {
+ return false;
+ } else {
+ return (mRatingValue == 1.0f);
+ }
+ }
+
+ /**
+ * Return the star-based rating value.
+ * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is
+ * not star-based, or if it is unrated.
+ */
+ public float getStarRating() {
+ switch (mRatingStyle) {
+ case RATING_3_STARS:
+ case RATING_4_STARS:
+ case RATING_5_STARS:
+ if (isRated()) {
+ return mRatingValue;
+ }
+ default:
+ return -1.0f;
+ }
+ }
+
+ /**
+ * Return the percentage-based rating value.
+ * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is
+ * not percentage-based, or if it is unrated.
+ */
+ public float getPercentRating() {
+ if ((mRatingStyle != RATING_PERCENTAGE) || !isRated()) {
+ return -1.0f;
+ } else {
+ return mRatingValue;
+ }
+ }
+}
diff --git a/android/media/SessionToken2.java b/android/media/SessionToken2.java
new file mode 100644
index 0000000..697a5a8
--- /dev/null
+++ b/android/media/SessionToken2.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.session.MediaSessionManager;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.text.TextUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represents an ongoing {@link MediaSession2} or a {@link MediaSessionService2}.
+ * If it's representing a session service, it may not be ongoing.
+ * <p>
+ * This may be passed to apps by the session owner to allow them to create a
+ * {@link MediaController2} to communicate with the session.
+ * <p>
+ * It can be also obtained by {@link MediaSessionManager}.
+ * @hide
+ */
+// TODO(jaewan): Unhide. SessionToken2?
+// TODO(jaewan): Move Token to updatable!
+// TODO(jaewan): Find better name for this (SessionToken or Session2Token)
+public final class SessionToken2 {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {TYPE_SESSION, TYPE_SESSION_SERVICE, TYPE_LIBRARY_SERVICE})
+ public @interface TokenType {
+ }
+
+ public static final int TYPE_SESSION = 0;
+ public static final int TYPE_SESSION_SERVICE = 1;
+ public static final int TYPE_LIBRARY_SERVICE = 2;
+
+ private static final String KEY_TYPE = "android.media.token.type";
+ private static final String KEY_PACKAGE_NAME = "android.media.token.package_name";
+ private static final String KEY_SERVICE_NAME = "android.media.token.service_name";
+ private static final String KEY_ID = "android.media.token.id";
+ private static final String KEY_SESSION_BINDER = "android.media.token.session_binder";
+
+ private final @TokenType int mType;
+ private final String mPackageName;
+ private final String mServiceName;
+ private final String mId;
+ private final IMediaSession2 mSessionBinder;
+
+ /**
+ * Constructor for the token.
+ *
+ * @hide
+ * @param type type
+ * @param packageName package name
+ * @param id id
+ * @param serviceName name of service. Can be {@code null} if it's not an service.
+ * @param sessionBinder binder for this session. Can be {@code null} if it's service.
+ * @hide
+ */
+ // TODO(jaewan): UID is also needed.
+ // TODO(jaewan): Unhide
+ public SessionToken2(@TokenType int type, @NonNull String packageName, @NonNull String id,
+ @Nullable String serviceName, @Nullable IMediaSession2 sessionBinder) {
+ // TODO(jaewan): Add sanity check.
+ mType = type;
+ mPackageName = packageName;
+ mId = id;
+ mServiceName = serviceName;
+ mSessionBinder = sessionBinder;
+ }
+
+ public int hashCode() {
+ final int prime = 31;
+ return mType
+ + prime * (mPackageName.hashCode()
+ + prime * (mId.hashCode()
+ + prime * ((mServiceName != null ? mServiceName.hashCode() : 0)
+ + prime * (mSessionBinder != null ? mSessionBinder.asBinder().hashCode() : 0))));
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ SessionToken2 other = (SessionToken2) obj;
+ if (!mPackageName.equals(other.getPackageName())
+ || !mServiceName.equals(other.getServiceName())
+ || !mId.equals(other.getId())
+ || mType != other.getType()) {
+ return false;
+ }
+ if (mSessionBinder == other.getSessionBinder()) {
+ return true;
+ } else if (mSessionBinder == null || other.getSessionBinder() == null) {
+ return false;
+ }
+ return mSessionBinder.asBinder().equals(other.getSessionBinder().asBinder());
+ }
+
+ @Override
+ public String toString() {
+ return "SessionToken {pkg=" + mPackageName + " id=" + mId + " type=" + mType
+ + " service=" + mServiceName + " binder=" + mSessionBinder + "}";
+ }
+
+ /**
+ * @return package name
+ */
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * @return id
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * @return type of the token
+ * @see #TYPE_SESSION
+ * @see #TYPE_SESSION_SERVICE
+ */
+ public @TokenType int getType() {
+ return mType;
+ }
+
+ /**
+ * @return session binder.
+ * @hide
+ */
+ public @Nullable IMediaSession2 getSessionBinder() {
+ return mSessionBinder;
+ }
+
+ /**
+ * @return service name if it's session service.
+ * @hide
+ */
+ public @Nullable String getServiceName() {
+ return mServiceName;
+ }
+
+ /**
+ * Create a token from the bundle, exported by {@link #toBundle()}.
+ *
+ * @param bundle
+ * @return
+ */
+ public static SessionToken2 fromBundle(@NonNull Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ final @TokenType int type = bundle.getInt(KEY_TYPE, -1);
+ final String packageName = bundle.getString(KEY_PACKAGE_NAME);
+ final String serviceName = bundle.getString(KEY_SERVICE_NAME);
+ final String id = bundle.getString(KEY_ID);
+ final IBinder sessionBinder = bundle.getBinder(KEY_SESSION_BINDER);
+
+ // Sanity check.
+ switch (type) {
+ case TYPE_SESSION:
+ if (!(sessionBinder instanceof IMediaSession2)) {
+ throw new IllegalArgumentException("Session needs sessionBinder");
+ }
+ break;
+ case TYPE_SESSION_SERVICE:
+ if (TextUtils.isEmpty(serviceName)) {
+ throw new IllegalArgumentException("Session service needs service name");
+ }
+ if (sessionBinder != null && !(sessionBinder instanceof IMediaSession2)) {
+ throw new IllegalArgumentException("Invalid session binder");
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid type");
+ }
+ if (TextUtils.isEmpty(packageName) || id == null) {
+ throw new IllegalArgumentException("Package name nor ID cannot be null.");
+ }
+ // TODO(jaewan): Revisit here when we add connection callback to the session for individual
+ // controller's permission check. With it, sessionBinder should be available
+ // if and only if for session, not session service.
+ return new SessionToken2(type, packageName, id, serviceName,
+ sessionBinder != null ? IMediaSession2.Stub.asInterface(sessionBinder) : null);
+ }
+
+ /**
+ * Create a {@link Bundle} from this token to share it across processes.
+ *
+ * @return Bundle
+ * @hide
+ */
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_PACKAGE_NAME, mPackageName);
+ bundle.putString(KEY_SERVICE_NAME, mServiceName);
+ bundle.putString(KEY_ID, mId);
+ bundle.putInt(KEY_TYPE, mType);
+ bundle.putBinder(KEY_SESSION_BINDER,
+ mSessionBinder != null ? mSessionBinder.asBinder() : null);
+ return bundle;
+ }
+}
diff --git a/android/media/TestServiceRegistry.java b/android/media/TestServiceRegistry.java
new file mode 100644
index 0000000..6f5512e
--- /dev/null
+++ b/android/media/TestServiceRegistry.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import static org.junit.Assert.fail;
+
+import android.media.MediaSession2.ControllerInfo;
+import android.media.TestUtils.SyncHandler;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.GuardedBy;
+
+/**
+ * Keeps the instance of currently running {@link MockMediaSessionService2}. And also provides
+ * a way to control them in one place.
+ * <p>
+ * It only support only one service at a time.
+ */
+public class TestServiceRegistry {
+ public interface ServiceInstanceChangedCallback {
+ void OnServiceInstanceChanged(MediaSessionService2 service);
+ }
+
+ @GuardedBy("TestServiceRegistry.class")
+ private static TestServiceRegistry sInstance;
+ @GuardedBy("TestServiceRegistry.class")
+ private MediaSessionService2 mService;
+ @GuardedBy("TestServiceRegistry.class")
+ private SyncHandler mHandler;
+ @GuardedBy("TestServiceRegistry.class")
+ private ControllerInfo mOnConnectControllerInfo;
+ @GuardedBy("TestServiceRegistry.class")
+ private ServiceInstanceChangedCallback mCallback;
+
+ public static TestServiceRegistry getInstance() {
+ synchronized (TestServiceRegistry.class) {
+ if (sInstance == null) {
+ sInstance = new TestServiceRegistry();
+ }
+ return sInstance;
+ }
+ }
+
+ public void setHandler(Handler handler) {
+ synchronized (TestServiceRegistry.class) {
+ mHandler = new SyncHandler(handler.getLooper());
+ }
+ }
+
+ public void setServiceInstanceChangedCallback(ServiceInstanceChangedCallback callback) {
+ synchronized (TestServiceRegistry.class) {
+ mCallback = callback;
+ }
+ }
+
+ public Handler getHandler() {
+ synchronized (TestServiceRegistry.class) {
+ return mHandler;
+ }
+ }
+
+ public void setServiceInstance(MediaSessionService2 service, ControllerInfo controller) {
+ synchronized (TestServiceRegistry.class) {
+ if (mService != null) {
+ fail("Previous service instance is still running. Clean up manually to ensure"
+ + " previoulsy running service doesn't break current test");
+ }
+ mService = service;
+ mOnConnectControllerInfo = controller;
+ if (mCallback != null) {
+ mCallback.OnServiceInstanceChanged(service);
+ }
+ }
+ }
+
+ public MediaSessionService2 getServiceInstance() {
+ synchronized (TestServiceRegistry.class) {
+ return mService;
+ }
+ }
+
+ public ControllerInfo getOnConnectControllerInfo() {
+ synchronized (TestServiceRegistry.class) {
+ return mOnConnectControllerInfo;
+ }
+ }
+
+
+ public void cleanUp() {
+ synchronized (TestServiceRegistry.class) {
+ final ServiceInstanceChangedCallback callback = mCallback;
+ if (mService != null) {
+ try {
+ if (mHandler.getLooper() == Looper.myLooper()) {
+ mService.getSession().close();
+ } else {
+ mHandler.postAndSync(() -> {
+ mService.getSession().close();
+ });
+ }
+ } catch (InterruptedException e) {
+ // No-op. Service containing session will die, but shouldn't be a huge issue.
+ }
+ // stopSelf() would not kill service while the binder connection established by
+ // bindService() exists, and close() above will do the job instead.
+ // So stopSelf() isn't really needed, but just for sure.
+ mService.stopSelf();
+ mService = null;
+ }
+ if (mHandler != null) {
+ mHandler.removeCallbacksAndMessages(null);
+ }
+ mCallback = null;
+ mOnConnectControllerInfo = null;
+
+ if (callback != null) {
+ callback.OnServiceInstanceChanged(null);
+ }
+ }
+ }
+}
diff --git a/android/media/TestUtils.java b/android/media/TestUtils.java
new file mode 100644
index 0000000..9a1fa10
--- /dev/null
+++ b/android/media/TestUtils.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.content.Context;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.os.Bundle;
+import android.os.Handler;
+
+import android.os.Looper;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Utilities for tests.
+ */
+public final class TestUtils {
+ private static final int WAIT_TIME_MS = 1000;
+ private static final int WAIT_SERVICE_TIME_MS = 5000;
+
+ /**
+ * Creates a {@link android.media.session.PlaybackState} with the given state.
+ *
+ * @param state one of the PlaybackState.STATE_xxx.
+ * @return a PlaybackState
+ */
+ public static PlaybackState2 createPlaybackState(int state) {
+ return new PlaybackState2(state, 0, 0, 1.0f,
+ 0, 0, null);
+ }
+
+ /**
+ * Finds the session with id in this test package.
+ *
+ * @param context
+ * @param id
+ * @return
+ */
+ // TODO(jaewan): Currently not working.
+ public static SessionToken2 getServiceToken(Context context, String id) {
+ MediaSessionManager manager =
+ (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
+ List<SessionToken2> tokens = manager.getSessionServiceTokens();
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ if (context.getPackageName().equals(token.getPackageName())
+ && id.equals(token.getId())) {
+ return token;
+ }
+ }
+ fail("Failed to find service");
+ return null;
+ }
+
+ /**
+ * Compares contents of two bundles.
+ *
+ * @param a a bundle
+ * @param b another bundle
+ * @return {@code true} if two bundles are the same. {@code false} otherwise. This may be
+ * incorrect if any bundle contains a bundle.
+ */
+ public static boolean equals(Bundle a, Bundle b) {
+ if (a == b) {
+ return true;
+ }
+ if (a == null || b == null) {
+ return false;
+ }
+ if (!a.keySet().containsAll(b.keySet())
+ || !b.keySet().containsAll(a.keySet())) {
+ return false;
+ }
+ for (String key : a.keySet()) {
+ if (!Objects.equals(a.get(key), b.get(key))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Handler that always waits until the Runnable finishes.
+ */
+ public static class SyncHandler extends Handler {
+ public SyncHandler(Looper looper) {
+ super(looper);
+ }
+
+ public void postAndSync(Runnable runnable) throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ if (getLooper() == Looper.myLooper()) {
+ runnable.run();
+ } else {
+ post(()->{
+ runnable.run();
+ latch.countDown();
+ });
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+ }
+}
diff --git a/android/media/session/MediaSessionManager.java b/android/media/session/MediaSessionManager.java
index b215825..81b4603 100644
--- a/android/media/session/MediaSessionManager.java
+++ b/android/media/session/MediaSessionManager.java
@@ -24,8 +24,12 @@
import android.content.ComponentName;
import android.content.Context;
import android.media.AudioManager;
+import android.media.IMediaSession2;
import android.media.IRemoteVolumeController;
-import android.media.session.ISessionManager;
+import android.media.MediaSession2;
+import android.media.MediaSessionService2;
+import android.media.SessionToken2;
+import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
@@ -38,6 +42,7 @@
import android.view.KeyEvent;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
/**
@@ -331,6 +336,101 @@
}
/**
+ * Called when a {@link MediaSession2} is created.
+ *
+ * @hide
+ */
+ // TODO(jaewan): System API
+ public SessionToken2 createSessionToken(@NonNull String callingPackage, @NonNull String id,
+ @NonNull IMediaSession2 binder) {
+ try {
+ Bundle bundle = mService.createSessionToken(callingPackage, id, binder);
+ return SessionToken2.fromBundle(bundle);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Cannot communicate with the service.", e);
+ }
+ return null;
+ }
+
+ /**
+ * Get {@link List} of {@link SessionToken2} whose sessions are active now. This list represents
+ * active sessions regardless of whether they're {@link MediaSession2} or
+ * {@link MediaSessionService2}.
+ *
+ * @return list of Tokens
+ * @hide
+ */
+ // TODO(jaewan): Unhide
+ // TODO(jaewan): Protect this with permission.
+ // TODO(jaewna): Add listener for change in lists.
+ public List<SessionToken2> getActiveSessionTokens() {
+ try {
+ List<Bundle> bundles = mService.getSessionTokens(
+ /* activeSessionOnly */ true, /* sessionServiceOnly */ false);
+ return toTokenList(bundles);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Cannot communicate with the service.", e);
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Get {@link List} of {@link SessionToken2} for {@link MediaSessionService2} regardless of their
+ * activeness. This list represents media apps that support background playback.
+ *
+ * @return list of Tokens
+ * @hide
+ */
+ // TODO(jaewan): Unhide
+ // TODO(jaewna): Add listener for change in lists.
+ public List<SessionToken2> getSessionServiceTokens() {
+ try {
+ List<Bundle> bundles = mService.getSessionTokens(
+ /* activeSessionOnly */ false, /* sessionServiceOnly */ true);
+ return toTokenList(bundles);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Cannot communicate with the service.", e);
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Get all {@link SessionToken2}s. This is the combined list of {@link #getActiveSessionTokens()}
+ * and {@link #getSessionServiceTokens}.
+ *
+ * @return list of Tokens
+ * @see #getActiveSessionTokens
+ * @see #getSessionServiceTokens
+ * @hide
+ */
+ // TODO(jaewan): Unhide
+ // TODO(jaewan): Protect this with permission.
+ // TODO(jaewna): Add listener for change in lists.
+ public List<SessionToken2> getAllSessionTokens() {
+ try {
+ List<Bundle> bundles = mService.getSessionTokens(
+ /* activeSessionOnly */ false, /* sessionServiceOnly */ false);
+ return toTokenList(bundles);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Cannot communicate with the service.", e);
+ return Collections.emptyList();
+ }
+ }
+
+ private static List<SessionToken2> toTokenList(List<Bundle> bundles) {
+ List<SessionToken2> tokens = new ArrayList<>();
+ if (bundles != null) {
+ for (int i = 0; i < bundles.size(); i++) {
+ SessionToken2 token = SessionToken2.fromBundle(bundles.get(i));
+ if (token != null) {
+ tokens.add(token);
+ }
+ }
+ }
+ return tokens;
+ }
+
+ /**
* Check if the global priority session is currently active. This can be
* used to decide if media keys should be sent to the session or to the app.
*
diff --git a/android/media/update/ApiLoader.java b/android/media/update/ApiLoader.java
new file mode 100644
index 0000000..b928e93
--- /dev/null
+++ b/android/media/update/ApiLoader.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.update;
+
+import android.content.res.Resources;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+/**
+ * @hide
+ */
+public final class ApiLoader {
+ private static Object sMediaLibrary;
+
+ private static final String UPDATE_PACKAGE = "com.android.media.update";
+ private static final String UPDATE_CLASS = "com.android.media.update.ApiFactory";
+ private static final String UPDATE_METHOD = "initialize";
+
+ private ApiLoader() { }
+
+ public static StaticProvider getProvider(Context context) {
+ try {
+ return (StaticProvider) getMediaLibraryImpl(context);
+ } catch (PackageManager.NameNotFoundException | ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // TODO This method may do I/O; Ensure it does not violate (emit warnings in) strict mode.
+ private static synchronized Object getMediaLibraryImpl(Context context)
+ throws PackageManager.NameNotFoundException, ReflectiveOperationException {
+ if (sMediaLibrary != null) return sMediaLibrary;
+
+ // TODO Figure out when to use which package (query media update service)
+ int flags = Build.IS_DEBUGGABLE ? 0 : PackageManager.MATCH_FACTORY_ONLY;
+ Context libContext = context.createApplicationContext(
+ context.getPackageManager().getPackageInfo(UPDATE_PACKAGE, flags).applicationInfo,
+ Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
+ sMediaLibrary = libContext.getClassLoader()
+ .loadClass(UPDATE_CLASS)
+ .getMethod(UPDATE_METHOD, Resources.class, Resources.Theme.class)
+ .invoke(null, libContext.getResources(), libContext.getTheme());
+ return sMediaLibrary;
+ }
+}
diff --git a/android/media/update/MediaBrowser2Provider.java b/android/media/update/MediaBrowser2Provider.java
new file mode 100644
index 0000000..e48711d
--- /dev/null
+++ b/android/media/update/MediaBrowser2Provider.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.update;
+
+import android.os.Bundle;
+
+/**
+ * @hide
+ */
+public interface MediaBrowser2Provider extends MediaController2Provider {
+ void getBrowserRoot_impl(Bundle rootHints);
+
+ void subscribe_impl(String parentId, Bundle options);
+ void unsubscribe_impl(String parentId, Bundle options);
+
+ void getItem_impl(String mediaId);
+ void getChildren_impl(String parentId, int page, int pageSize, Bundle options);
+ void search_impl(String query, int page, int pageSize, Bundle extras);
+}
diff --git a/android/media/update/MediaControlView2Provider.java b/android/media/update/MediaControlView2Provider.java
new file mode 100644
index 0000000..6b38c92
--- /dev/null
+++ b/android/media/update/MediaControlView2Provider.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.update;
+
+import android.annotation.SystemApi;
+import android.media.session.MediaController;
+import android.view.View;
+
+/**
+ * Interface for connecting the public API to an updatable implementation.
+ *
+ * Each instance object is connected to one corresponding updatable object which implements the
+ * runtime behavior of that class. There should a corresponding provider method for all public
+ * methods.
+ *
+ * All methods behave as per their namesake in the public API.
+ *
+ * @see android.widget.MediaControlView2
+ *
+ * @hide
+ */
+// TODO @SystemApi
+public interface MediaControlView2Provider extends ViewProvider {
+ void setController_impl(MediaController controller);
+ void show_impl();
+ void show_impl(int timeout);
+ boolean isShowing_impl();
+ void hide_impl();
+ void showSubtitle_impl();
+ void hideSubtitle_impl();
+ void setPrevNextListeners_impl(View.OnClickListener next, View.OnClickListener prev);
+ void setButtonVisibility_impl(int button, boolean visible);
+}
diff --git a/android/media/update/MediaController2Provider.java b/android/media/update/MediaController2Provider.java
new file mode 100644
index 0000000..c5f6b96
--- /dev/null
+++ b/android/media/update/MediaController2Provider.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.update;
+
+import android.app.PendingIntent;
+import android.media.MediaController2.PlaybackInfo;
+import android.media.MediaItem2;
+import android.media.MediaSession2.Command;
+import android.media.MediaSession2.PlaylistParam;
+import android.media.PlaybackState2;
+import android.media.Rating2;
+import android.media.SessionToken2;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import java.util.List;
+
+/**
+ * @hide
+ */
+public interface MediaController2Provider extends TransportControlProvider {
+ void close_impl();
+ SessionToken2 getSessionToken_impl();
+ boolean isConnected_impl();
+
+ PendingIntent getSessionActivity_impl();
+ int getRatingType_impl();
+
+ void setVolumeTo_impl(int value, int flags);
+ void adjustVolume_impl(int direction, int flags);
+ PlaybackInfo getPlaybackInfo_impl();
+
+ void prepareFromUri_impl(Uri uri, Bundle extras);
+ void prepareFromSearch_impl(String query, Bundle extras);
+ void prepareMediaId_impl(String mediaId, Bundle extras);
+ void playFromSearch_impl(String query, Bundle extras);
+ void playFromUri_impl(String uri, Bundle extras);
+ void playFromMediaId_impl(String mediaId, Bundle extras);
+
+ void setRating_impl(Rating2 rating);
+ void sendCustomCommand_impl(Command command, Bundle args, ResultReceiver cb);
+ List<MediaItem2> getPlaylist_impl();
+
+ void removePlaylistItem_impl(MediaItem2 index);
+ void addPlaylistItem_impl(int index, MediaItem2 item);
+
+ PlaylistParam getPlaylistParam_impl();
+ PlaybackState2 getPlaybackState_impl();
+}
diff --git a/android/media/update/MediaLibraryService2Provider.java b/android/media/update/MediaLibraryService2Provider.java
new file mode 100644
index 0000000..dac5784
--- /dev/null
+++ b/android/media/update/MediaLibraryService2Provider.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.update;
+
+import android.media.MediaSession2.ControllerInfo;
+import android.os.Bundle; /**
+ * @hide
+ */
+public interface MediaLibraryService2Provider extends MediaSessionService2Provider {
+ // Nothing new for now
+
+ interface MediaLibrarySessionProvider extends MediaSession2Provider {
+ void notifyChildrenChanged_impl(ControllerInfo controller, String parentId, Bundle options);
+ void notifyChildrenChanged_impl(String parentId, Bundle options);
+ }
+}
diff --git a/android/media/update/MediaSession2Provider.java b/android/media/update/MediaSession2Provider.java
new file mode 100644
index 0000000..2a68ad1
--- /dev/null
+++ b/android/media/update/MediaSession2Provider.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.update;
+
+import android.media.AudioAttributes;
+import android.media.MediaItem2;
+import android.media.MediaPlayerBase;
+import android.media.MediaSession2;
+import android.media.MediaSession2.Command;
+import android.media.MediaSession2.CommandButton;
+import android.media.MediaSession2.CommandGroup;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.SessionToken2;
+import android.media.VolumeProvider;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import java.util.List;
+
+/**
+ * @hide
+ */
+public interface MediaSession2Provider extends TransportControlProvider {
+ void close_impl();
+ void setPlayer_impl(MediaPlayerBase player);
+ void setPlayer_impl(MediaPlayerBase player, VolumeProvider volumeProvider);
+ MediaPlayerBase getPlayer_impl();
+ SessionToken2 getToken_impl();
+ List<ControllerInfo> getConnectedControllers_impl();
+ void setCustomLayout_impl(ControllerInfo controller, List<CommandButton> layout);
+ void setAudioAttributes_impl(AudioAttributes attributes);
+ void setAudioFocusRequest_impl(int focusGain);
+
+ void setAllowedCommands_impl(ControllerInfo controller, CommandGroup commands);
+ void notifyMetadataChanged_impl();
+ void sendCustomCommand_impl(ControllerInfo controller, Command command, Bundle args,
+ ResultReceiver receiver);
+ void sendCustomCommand_impl(Command command, Bundle args);
+ void setPlaylist_impl(List<MediaItem2> playlist, MediaSession2.PlaylistParam param);
+
+ /**
+ * @hide
+ */
+ interface ControllerInfoProvider {
+ String getPackageName_impl();
+ int getUid_impl();
+ boolean isTrusted_impl();
+ int hashCode_impl();
+ boolean equals_impl(ControllerInfoProvider obj);
+ }
+}
diff --git a/android/media/update/MediaSessionService2Provider.java b/android/media/update/MediaSessionService2Provider.java
new file mode 100644
index 0000000..a6b462b
--- /dev/null
+++ b/android/media/update/MediaSessionService2Provider.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.update;
+
+import android.content.Intent;
+import android.media.MediaSession2;
+import android.media.MediaSessionService2.MediaNotification;
+import android.media.PlaybackState2;
+import android.os.IBinder;
+
+/**
+ * @hide
+ */
+public interface MediaSessionService2Provider {
+ MediaSession2 getSession_impl();
+ MediaNotification onUpdateNotification_impl(PlaybackState2 state);
+
+ // Service
+ void onCreate_impl();
+ IBinder onBind_impl(Intent intent);
+}
diff --git a/android/media/update/StaticProvider.java b/android/media/update/StaticProvider.java
new file mode 100644
index 0000000..7c222c3
--- /dev/null
+++ b/android/media/update/StaticProvider.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.update;
+
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.media.IMediaSession2Callback;
+import android.media.MediaBrowser2;
+import android.media.MediaBrowser2.BrowserCallback;
+import android.media.MediaController2;
+import android.media.MediaController2.ControllerCallback;
+import android.media.MediaLibraryService2;
+import android.media.MediaLibraryService2.MediaLibrarySession;
+import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
+import android.media.MediaPlayerBase;
+import android.media.MediaSession2;
+import android.media.MediaSession2.SessionCallback;
+import android.media.MediaSessionService2;
+import android.media.SessionToken2;
+import android.media.VolumeProvider;
+import android.media.update.MediaLibraryService2Provider.MediaLibrarySessionProvider;
+import android.media.update.MediaSession2Provider.ControllerInfoProvider;
+import android.util.AttributeSet;
+import android.widget.MediaControlView2;
+import android.widget.VideoView2;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Interface for connecting the public API to an updatable implementation.
+ *
+ * This interface provides access to constructors and static methods that are otherwise not directly
+ * accessible via an implementation object.
+ *
+ * @hide
+ */
+// TODO @SystemApi
+public interface StaticProvider {
+ MediaControlView2Provider createMediaControlView2(
+ MediaControlView2 instance, ViewProvider superProvider);
+ VideoView2Provider createVideoView2(
+ VideoView2 instance, ViewProvider superProvider,
+ @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes);
+
+ MediaSession2Provider createMediaSession2(MediaSession2 mediaSession2, Context context,
+ MediaPlayerBase player, String id, Executor callbackExecutor, SessionCallback callback,
+ VolumeProvider volumeProvider, int ratingType,
+ PendingIntent sessionActivity);
+ ControllerInfoProvider createMediaSession2ControllerInfoProvider(
+ MediaSession2.ControllerInfo instance, Context context, int uid, int pid,
+ String packageName, IMediaSession2Callback callback);
+ MediaController2Provider createMediaController2(
+ MediaController2 instance, Context context, SessionToken2 token,
+ ControllerCallback callback, Executor executor);
+ MediaBrowser2Provider createMediaBrowser2(
+ MediaBrowser2 instance, Context context, SessionToken2 token,
+ BrowserCallback callback, Executor executor);
+ MediaSessionService2Provider createMediaSessionService2(
+ MediaSessionService2 instance);
+ MediaSessionService2Provider createMediaLibraryService2(
+ MediaLibraryService2 instance);
+ MediaLibrarySessionProvider createMediaLibraryService2MediaLibrarySession(
+ MediaLibrarySession instance, Context context, MediaPlayerBase player, String id,
+ Executor callbackExecutor, MediaLibrarySessionCallback callback,
+ VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity);
+}
diff --git a/android/media/update/TransportControlProvider.java b/android/media/update/TransportControlProvider.java
new file mode 100644
index 0000000..5217a9d
--- /dev/null
+++ b/android/media/update/TransportControlProvider.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.update;
+
+import android.media.MediaPlayerBase;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+
+/**
+ * @hide
+ */
+// TODO(jaewan): SystemApi
+public interface TransportControlProvider {
+ void play_impl();
+ void pause_impl();
+ void stop_impl();
+ void skipToPrevious_impl();
+ void skipToNext_impl();
+
+ void prepare_impl();
+ void fastForward_impl();
+ void rewind_impl();
+ void seekTo_impl(long pos);
+ void setCurrentPlaylistItem_impl(int index);
+}
diff --git a/android/media/update/VideoView2Provider.java b/android/media/update/VideoView2Provider.java
new file mode 100644
index 0000000..416ea98
--- /dev/null
+++ b/android/media/update/VideoView2Provider.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.update;
+
+import android.media.AudioAttributes;
+import android.media.MediaPlayerBase;
+import android.net.Uri;
+import android.widget.MediaControlView2;
+import android.widget.VideoView2;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Interface for connecting the public API to an updatable implementation.
+ *
+ * Each instance object is connected to one corresponding updatable object which implements the
+ * runtime behavior of that class. There should a corresponding provider method for all public
+ * methods.
+ *
+ * All methods behave as per their namesake in the public API.
+ *
+ * @see android.widget.VideoView2
+ *
+ * @hide
+ */
+// TODO @SystemApi
+public interface VideoView2Provider extends ViewProvider {
+ void setMediaControlView2_impl(MediaControlView2 mediaControlView);
+ MediaControlView2 getMediaControlView2_impl();
+ void start_impl();
+ void pause_impl();
+ int getDuration_impl();
+ int getCurrentPosition_impl();
+ void seekTo_impl(int msec);
+ boolean isPlaying_impl();
+ int getBufferPercentage_impl();
+ int getAudioSessionId_impl();
+ void showSubtitle_impl();
+ void hideSubtitle_impl();
+ void setFullScreen_impl(boolean fullScreen);
+ void setSpeed_impl(float speed);
+ float getSpeed_impl();
+ void setAudioFocusRequest_impl(int focusGain);
+ void setAudioAttributes_impl(AudioAttributes attributes);
+ void setRouteAttributes_impl(List<String> routeCategories, MediaPlayerBase player);
+ void setVideoPath_impl(String path);
+ void setVideoURI_impl(Uri uri);
+ void setVideoURI_impl(Uri uri, Map<String, String> headers);
+ void setViewType_impl(int viewType);
+ int getViewType_impl();
+ void stopPlayback_impl();
+ void setOnPreparedListener_impl(VideoView2.OnPreparedListener l);
+ void setOnCompletionListener_impl(VideoView2.OnCompletionListener l);
+ void setOnErrorListener_impl(VideoView2.OnErrorListener l);
+ void setOnInfoListener_impl(VideoView2.OnInfoListener l);
+ void setOnViewTypeChangedListener_impl(VideoView2.OnViewTypeChangedListener l);
+ void setFullScreenChangedListener_impl(VideoView2.OnFullScreenChangedListener l);
+}
diff --git a/android/media/update/ViewProvider.java b/android/media/update/ViewProvider.java
new file mode 100644
index 0000000..78c5b36
--- /dev/null
+++ b/android/media/update/ViewProvider.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.update;
+
+import android.annotation.SystemApi;
+import android.graphics.Canvas;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+/**
+ * Interface for connecting the public API to an updatable implementation.
+ *
+ * Each instance object is connected to one corresponding updatable object which implements the
+ * runtime behavior of that class. There should a corresponding provider method for all public
+ * methods.
+ *
+ * All methods behave as per their namesake in the public API.
+ *
+ * @see android.view.View
+ *
+ * @hide
+ */
+// TODO @SystemApi
+public interface ViewProvider {
+ // TODO Add more (all?) methods from View
+ void onAttachedToWindow_impl();
+ void onDetachedFromWindow_impl();
+ CharSequence getAccessibilityClassName_impl();
+ boolean onTouchEvent_impl(MotionEvent ev);
+ boolean onTrackballEvent_impl(MotionEvent ev);
+ boolean onKeyDown_impl(int keyCode, KeyEvent event);
+ void onFinishInflate_impl();
+ boolean dispatchKeyEvent_impl(KeyEvent event);
+ void setEnabled_impl(boolean enabled);
+}
diff --git a/android/net/ConnectivityManager.java b/android/net/ConnectivityManager.java
index 11d338d..166342d 100644
--- a/android/net/ConnectivityManager.java
+++ b/android/net/ConnectivityManager.java
@@ -3763,4 +3763,20 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * The network watchlist is a list of domains and IP addresses that are associated with
+ * potentially harmful apps. This method returns the hash of the watchlist currently
+ * used by the system.
+ *
+ * @return Hash of network watchlist config file. Null if config does not exist.
+ */
+ public byte[] getNetworkWatchlistConfigHash() {
+ try {
+ return mService.getNetworkWatchlistConfigHash();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to get watchlist config hash");
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/android/net/IpSecAlgorithm.java b/android/net/IpSecAlgorithm.java
index f82627b..c69a4d4 100644
--- a/android/net/IpSecAlgorithm.java
+++ b/android/net/IpSecAlgorithm.java
@@ -231,13 +231,44 @@
}
}
+ /** @hide */
+ public boolean isAuthentication() {
+ switch (getName()) {
+ // Fallthrough
+ case AUTH_HMAC_MD5:
+ case AUTH_HMAC_SHA1:
+ case AUTH_HMAC_SHA256:
+ case AUTH_HMAC_SHA384:
+ case AUTH_HMAC_SHA512:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /** @hide */
+ public boolean isEncryption() {
+ return getName().equals(CRYPT_AES_CBC);
+ }
+
+ /** @hide */
+ public boolean isAead() {
+ return getName().equals(AUTH_CRYPT_AES_GCM);
+ }
+
+ // Because encryption keys are sensitive and userdebug builds are used by large user pools
+ // such as beta testers, we only allow sensitive info such as keys on eng builds.
+ private static boolean isUnsafeBuild() {
+ return Build.IS_DEBUGGABLE && Build.IS_ENG;
+ }
+
@Override
public String toString() {
return new StringBuilder()
.append("{mName=")
.append(mName)
.append(", mKey=")
- .append(Build.IS_DEBUGGABLE ? HexDump.toHexString(mKey) : "<hidden>")
+ .append(isUnsafeBuild() ? HexDump.toHexString(mKey) : "<hidden>")
.append(", mTruncLenBits=")
.append(mTruncLenBits)
.append("}")
diff --git a/android/net/IpSecConfig.java b/android/net/IpSecConfig.java
index e6cd3fc..6a262e2 100644
--- a/android/net/IpSecConfig.java
+++ b/android/net/IpSecConfig.java
@@ -32,59 +32,29 @@
// MODE_TRANSPORT or MODE_TUNNEL
private int mMode = IpSecTransform.MODE_TRANSPORT;
- // Needs to be valid only for tunnel mode
// Preventing this from being null simplifies Java->Native binder
- private String mLocalAddress = "";
+ private String mSourceAddress = "";
// Preventing this from being null simplifies Java->Native binder
- private String mRemoteAddress = "";
+ private String mDestinationAddress = "";
// The underlying Network that represents the "gateway" Network
// for outbound packets. It may also be used to select packets.
private Network mNetwork;
- /**
- * This class captures the parameters that specifically apply to inbound or outbound traffic.
- */
- public static class Flow {
- // Minimum requirements for identifying a transform
- // SPI identifying the IPsec flow in packet processing
- // and a remote IP address
- private int mSpiResourceId = IpSecManager.INVALID_RESOURCE_ID;
+ // Minimum requirements for identifying a transform
+ // SPI identifying the IPsec SA in packet processing
+ // and a destination IP address
+ private int mSpiResourceId = IpSecManager.INVALID_RESOURCE_ID;
- // Encryption Algorithm
- private IpSecAlgorithm mEncryption;
+ // Encryption Algorithm
+ private IpSecAlgorithm mEncryption;
- // Authentication Algorithm
- private IpSecAlgorithm mAuthentication;
+ // Authentication Algorithm
+ private IpSecAlgorithm mAuthentication;
- // Authenticated Encryption Algorithm
- private IpSecAlgorithm mAuthenticatedEncryption;
-
- @Override
- public String toString() {
- return new StringBuilder()
- .append("{mSpiResourceId=")
- .append(mSpiResourceId)
- .append(", mEncryption=")
- .append(mEncryption)
- .append(", mAuthentication=")
- .append(mAuthentication)
- .append(", mAuthenticatedEncryption=")
- .append(mAuthenticatedEncryption)
- .append("}")
- .toString();
- }
-
- static boolean equals(IpSecConfig.Flow lhs, IpSecConfig.Flow rhs) {
- if (lhs == null || rhs == null) return (lhs == rhs);
- return (lhs.mSpiResourceId == rhs.mSpiResourceId
- && IpSecAlgorithm.equals(lhs.mEncryption, rhs.mEncryption)
- && IpSecAlgorithm.equals(lhs.mAuthentication, rhs.mAuthentication));
- }
- }
-
- private final Flow[] mFlow = new Flow[] {new Flow(), new Flow()};
+ // Authenticated Encryption Algorithm
+ private IpSecAlgorithm mAuthenticatedEncryption;
// For tunnel mode IPv4 UDP Encapsulation
// IpSecTransform#ENCAP_ESP_*, such as ENCAP_ESP_OVER_UDP_IKE
@@ -95,47 +65,46 @@
// An interval, in seconds between the NattKeepalive packets
private int mNattKeepaliveInterval;
+ // XFRM mark and mask
+ private int mMarkValue;
+ private int mMarkMask;
+
/** Set the mode for this IPsec transform */
public void setMode(int mode) {
mMode = mode;
}
- /** Set the local IP address for Tunnel mode */
- public void setLocalAddress(String localAddress) {
- if (localAddress == null) {
- throw new IllegalArgumentException("localAddress may not be null!");
- }
- mLocalAddress = localAddress;
+ /** Set the source IP addres for this IPsec transform */
+ public void setSourceAddress(String sourceAddress) {
+ mSourceAddress = sourceAddress;
}
- /** Set the remote IP address for this IPsec transform */
- public void setRemoteAddress(String remoteAddress) {
- if (remoteAddress == null) {
- throw new IllegalArgumentException("remoteAddress may not be null!");
- }
- mRemoteAddress = remoteAddress;
+ /** Set the destination IP address for this IPsec transform */
+ public void setDestinationAddress(String destinationAddress) {
+ mDestinationAddress = destinationAddress;
}
- /** Set the SPI for a given direction by resource ID */
- public void setSpiResourceId(int direction, int resourceId) {
- mFlow[direction].mSpiResourceId = resourceId;
+ /** Set the SPI by resource ID */
+ public void setSpiResourceId(int resourceId) {
+ mSpiResourceId = resourceId;
}
- /** Set the encryption algorithm for a given direction */
- public void setEncryption(int direction, IpSecAlgorithm encryption) {
- mFlow[direction].mEncryption = encryption;
+ /** Set the encryption algorithm */
+ public void setEncryption(IpSecAlgorithm encryption) {
+ mEncryption = encryption;
}
- /** Set the authentication algorithm for a given direction */
- public void setAuthentication(int direction, IpSecAlgorithm authentication) {
- mFlow[direction].mAuthentication = authentication;
+ /** Set the authentication algorithm */
+ public void setAuthentication(IpSecAlgorithm authentication) {
+ mAuthentication = authentication;
}
- /** Set the authenticated encryption algorithm for a given direction */
- public void setAuthenticatedEncryption(int direction, IpSecAlgorithm authenticatedEncryption) {
- mFlow[direction].mAuthenticatedEncryption = authenticatedEncryption;
+ /** Set the authenticated encryption algorithm */
+ public void setAuthenticatedEncryption(IpSecAlgorithm authenticatedEncryption) {
+ mAuthenticatedEncryption = authenticatedEncryption;
}
+ /** Set the underlying network that will carry traffic for this transform */
public void setNetwork(Network network) {
mNetwork = network;
}
@@ -156,33 +125,41 @@
mNattKeepaliveInterval = interval;
}
+ public void setMarkValue(int mark) {
+ mMarkValue = mark;
+ }
+
+ public void setMarkMask(int mask) {
+ mMarkMask = mask;
+ }
+
// Transport or Tunnel
public int getMode() {
return mMode;
}
- public String getLocalAddress() {
- return mLocalAddress;
+ public String getSourceAddress() {
+ return mSourceAddress;
}
- public int getSpiResourceId(int direction) {
- return mFlow[direction].mSpiResourceId;
+ public int getSpiResourceId() {
+ return mSpiResourceId;
}
- public String getRemoteAddress() {
- return mRemoteAddress;
+ public String getDestinationAddress() {
+ return mDestinationAddress;
}
- public IpSecAlgorithm getEncryption(int direction) {
- return mFlow[direction].mEncryption;
+ public IpSecAlgorithm getEncryption() {
+ return mEncryption;
}
- public IpSecAlgorithm getAuthentication(int direction) {
- return mFlow[direction].mAuthentication;
+ public IpSecAlgorithm getAuthentication() {
+ return mAuthentication;
}
- public IpSecAlgorithm getAuthenticatedEncryption(int direction) {
- return mFlow[direction].mAuthenticatedEncryption;
+ public IpSecAlgorithm getAuthenticatedEncryption() {
+ return mAuthenticatedEncryption;
}
public Network getNetwork() {
@@ -205,6 +182,14 @@
return mNattKeepaliveInterval;
}
+ public int getMarkValue() {
+ return mMarkValue;
+ }
+
+ public int getMarkMask() {
+ return mMarkMask;
+ }
+
// Parcelable Methods
@Override
@@ -215,21 +200,19 @@
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(mMode);
- out.writeString(mLocalAddress);
- out.writeString(mRemoteAddress);
+ out.writeString(mSourceAddress);
+ out.writeString(mDestinationAddress);
out.writeParcelable(mNetwork, flags);
- out.writeInt(mFlow[IpSecTransform.DIRECTION_IN].mSpiResourceId);
- out.writeParcelable(mFlow[IpSecTransform.DIRECTION_IN].mEncryption, flags);
- out.writeParcelable(mFlow[IpSecTransform.DIRECTION_IN].mAuthentication, flags);
- out.writeParcelable(mFlow[IpSecTransform.DIRECTION_IN].mAuthenticatedEncryption, flags);
- out.writeInt(mFlow[IpSecTransform.DIRECTION_OUT].mSpiResourceId);
- out.writeParcelable(mFlow[IpSecTransform.DIRECTION_OUT].mEncryption, flags);
- out.writeParcelable(mFlow[IpSecTransform.DIRECTION_OUT].mAuthentication, flags);
- out.writeParcelable(mFlow[IpSecTransform.DIRECTION_OUT].mAuthenticatedEncryption, flags);
+ out.writeInt(mSpiResourceId);
+ out.writeParcelable(mEncryption, flags);
+ out.writeParcelable(mAuthentication, flags);
+ out.writeParcelable(mAuthenticatedEncryption, flags);
out.writeInt(mEncapType);
out.writeInt(mEncapSocketResourceId);
out.writeInt(mEncapRemotePort);
out.writeInt(mNattKeepaliveInterval);
+ out.writeInt(mMarkValue);
+ out.writeInt(mMarkMask);
}
@VisibleForTesting
@@ -237,27 +220,22 @@
private IpSecConfig(Parcel in) {
mMode = in.readInt();
- mLocalAddress = in.readString();
- mRemoteAddress = in.readString();
+ mSourceAddress = in.readString();
+ mDestinationAddress = in.readString();
mNetwork = (Network) in.readParcelable(Network.class.getClassLoader());
- mFlow[IpSecTransform.DIRECTION_IN].mSpiResourceId = in.readInt();
- mFlow[IpSecTransform.DIRECTION_IN].mEncryption =
+ mSpiResourceId = in.readInt();
+ mEncryption =
(IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
- mFlow[IpSecTransform.DIRECTION_IN].mAuthentication =
+ mAuthentication =
(IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
- mFlow[IpSecTransform.DIRECTION_IN].mAuthenticatedEncryption =
- (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
- mFlow[IpSecTransform.DIRECTION_OUT].mSpiResourceId = in.readInt();
- mFlow[IpSecTransform.DIRECTION_OUT].mEncryption =
- (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
- mFlow[IpSecTransform.DIRECTION_OUT].mAuthentication =
- (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
- mFlow[IpSecTransform.DIRECTION_OUT].mAuthenticatedEncryption =
+ mAuthenticatedEncryption =
(IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
mEncapType = in.readInt();
mEncapSocketResourceId = in.readInt();
mEncapRemotePort = in.readInt();
mNattKeepaliveInterval = in.readInt();
+ mMarkValue = in.readInt();
+ mMarkMask = in.readInt();
}
@Override
@@ -266,10 +244,10 @@
strBuilder
.append("{mMode=")
.append(mMode == IpSecTransform.MODE_TUNNEL ? "TUNNEL" : "TRANSPORT")
- .append(", mLocalAddress=")
- .append(mLocalAddress)
- .append(", mRemoteAddress=")
- .append(mRemoteAddress)
+ .append(", mSourceAddress=")
+ .append(mSourceAddress)
+ .append(", mDestinationAddress=")
+ .append(mDestinationAddress)
.append(", mNetwork=")
.append(mNetwork)
.append(", mEncapType=")
@@ -280,10 +258,18 @@
.append(mEncapRemotePort)
.append(", mNattKeepaliveInterval=")
.append(mNattKeepaliveInterval)
- .append(", mFlow[OUT]=")
- .append(mFlow[IpSecTransform.DIRECTION_OUT])
- .append(", mFlow[IN]=")
- .append(mFlow[IpSecTransform.DIRECTION_IN])
+ .append("{mSpiResourceId=")
+ .append(mSpiResourceId)
+ .append(", mEncryption=")
+ .append(mEncryption)
+ .append(", mAuthentication=")
+ .append(mAuthentication)
+ .append(", mAuthenticatedEncryption=")
+ .append(mAuthenticatedEncryption)
+ .append(", mMarkValue=")
+ .append(mMarkValue)
+ .append(", mMarkMask=")
+ .append(mMarkMask)
.append("}");
return strBuilder.toString();
@@ -305,17 +291,20 @@
public static boolean equals(IpSecConfig lhs, IpSecConfig rhs) {
if (lhs == null || rhs == null) return (lhs == rhs);
return (lhs.mMode == rhs.mMode
- && lhs.mLocalAddress.equals(rhs.mLocalAddress)
- && lhs.mRemoteAddress.equals(rhs.mRemoteAddress)
+ && lhs.mSourceAddress.equals(rhs.mSourceAddress)
+ && lhs.mDestinationAddress.equals(rhs.mDestinationAddress)
&& ((lhs.mNetwork != null && lhs.mNetwork.equals(rhs.mNetwork))
|| (lhs.mNetwork == rhs.mNetwork))
&& lhs.mEncapType == rhs.mEncapType
&& lhs.mEncapSocketResourceId == rhs.mEncapSocketResourceId
&& lhs.mEncapRemotePort == rhs.mEncapRemotePort
&& lhs.mNattKeepaliveInterval == rhs.mNattKeepaliveInterval
- && IpSecConfig.Flow.equals(lhs.mFlow[IpSecTransform.DIRECTION_OUT],
- rhs.mFlow[IpSecTransform.DIRECTION_OUT])
- && IpSecConfig.Flow.equals(lhs.mFlow[IpSecTransform.DIRECTION_IN],
- rhs.mFlow[IpSecTransform.DIRECTION_IN]));
+ && lhs.mSpiResourceId == rhs.mSpiResourceId
+ && IpSecAlgorithm.equals(lhs.mEncryption, rhs.mEncryption)
+ && IpSecAlgorithm.equals(
+ lhs.mAuthenticatedEncryption, rhs.mAuthenticatedEncryption)
+ && IpSecAlgorithm.equals(lhs.mAuthentication, rhs.mAuthentication)
+ && lhs.mMarkValue == rhs.mMarkValue
+ && lhs.mMarkMask == rhs.mMarkMask);
}
}
diff --git a/android/net/IpSecManager.java b/android/net/IpSecManager.java
index 6a4b891..24a078f 100644
--- a/android/net/IpSecManager.java
+++ b/android/net/IpSecManager.java
@@ -17,7 +17,9 @@
import static com.android.internal.util.Preconditions.checkNotNull;
+import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.content.Context;
@@ -33,6 +35,8 @@
import java.io.FileDescriptor;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
@@ -53,6 +57,23 @@
private static final String TAG = "IpSecManager";
/**
+ * For direction-specific attributes of an {@link IpSecTransform}, indicates that an attribute
+ * applies to traffic towards the host.
+ */
+ public static final int DIRECTION_IN = 0;
+
+ /**
+ * For direction-specific attributes of an {@link IpSecTransform}, indicates that an attribute
+ * applies to traffic from the host.
+ */
+ public static final int DIRECTION_OUT = 1;
+
+ /** @hide */
+ @IntDef(value = {DIRECTION_IN, DIRECTION_OUT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PolicyDirection {}
+
+ /**
* The Security Parameter Index (SPI) 0 indicates an unknown or invalid index.
*
* <p>No IPsec packet may contain an SPI of 0.
@@ -69,7 +90,7 @@
}
/** @hide */
- public static final int INVALID_RESOURCE_ID = 0;
+ public static final int INVALID_RESOURCE_ID = -1;
/**
* Thrown to indicate that a requested SPI is in use.
@@ -125,10 +146,10 @@
*/
public static final class SecurityParameterIndex implements AutoCloseable {
private final IIpSecService mService;
- private final InetAddress mRemoteAddress;
+ private final InetAddress mDestinationAddress;
private final CloseGuard mCloseGuard = CloseGuard.get();
private int mSpi = INVALID_SECURITY_PARAMETER_INDEX;
- private int mResourceId;
+ private int mResourceId = INVALID_RESOURCE_ID;
/** Get the underlying SPI held by this object. */
public int getSpi() {
@@ -146,6 +167,7 @@
public void close() {
try {
mService.releaseSecurityParameterIndex(mResourceId);
+ mResourceId = INVALID_RESOURCE_ID;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -163,14 +185,14 @@
}
private SecurityParameterIndex(
- @NonNull IIpSecService service, int direction, InetAddress remoteAddress, int spi)
+ @NonNull IIpSecService service, InetAddress destinationAddress, int spi)
throws ResourceUnavailableException, SpiUnavailableException {
mService = service;
- mRemoteAddress = remoteAddress;
+ mDestinationAddress = destinationAddress;
try {
IpSecSpiResponse result =
mService.allocateSecurityParameterIndex(
- direction, remoteAddress.getHostAddress(), spi, new Binder());
+ destinationAddress.getHostAddress(), spi, new Binder());
if (result == null) {
throw new NullPointerException("Received null response from IpSecService");
@@ -215,25 +237,23 @@
}
/**
- * Reserve a random SPI for traffic bound to or from the specified remote address.
+ * Reserve a random SPI for traffic bound to or from the specified destination address.
*
* <p>If successful, this SPI is guaranteed available until released by a call to {@link
* SecurityParameterIndex#close()}.
*
- * @param direction {@link IpSecTransform#DIRECTION_IN} or {@link IpSecTransform#DIRECTION_OUT}
- * @param remoteAddress address of the remote. SPIs must be unique for each remoteAddress
+ * @param destinationAddress the destination address for traffic bearing the requested SPI.
+ * For inbound traffic, the destination should be an address currently assigned on-device.
* @return the reserved SecurityParameterIndex
- * @throws ResourceUnavailableException indicating that too many SPIs are currently allocated
- * for this user
- * @throws SpiUnavailableException indicating that a particular SPI cannot be reserved
+ * @throws {@link #ResourceUnavailableException} indicating that too many SPIs are
+ * currently allocated for this user
*/
- public SecurityParameterIndex allocateSecurityParameterIndex(
- int direction, InetAddress remoteAddress) throws ResourceUnavailableException {
+ public SecurityParameterIndex allocateSecurityParameterIndex(InetAddress destinationAddress)
+ throws ResourceUnavailableException {
try {
return new SecurityParameterIndex(
mService,
- direction,
- remoteAddress,
+ destinationAddress,
IpSecManager.INVALID_SECURITY_PARAMETER_INDEX);
} catch (SpiUnavailableException unlikely) {
throw new ResourceUnavailableException("No SPIs available");
@@ -241,26 +261,27 @@
}
/**
- * Reserve the requested SPI for traffic bound to or from the specified remote address.
+ * Reserve the requested SPI for traffic bound to or from the specified destination address.
*
* <p>If successful, this SPI is guaranteed available until released by a call to {@link
* SecurityParameterIndex#close()}.
*
- * @param direction {@link IpSecTransform#DIRECTION_IN} or {@link IpSecTransform#DIRECTION_OUT}
- * @param remoteAddress address of the remote. SPIs must be unique for each remoteAddress
+ * @param destinationAddress the destination address for traffic bearing the requested SPI.
+ * For inbound traffic, the destination should be an address currently assigned on-device.
* @param requestedSpi the requested SPI, or '0' to allocate a random SPI
* @return the reserved SecurityParameterIndex
- * @throws ResourceUnavailableException indicating that too many SPIs are currently allocated
- * for this user
- * @throws SpiUnavailableException indicating that the requested SPI could not be reserved
+ * @throws {@link #ResourceUnavailableException} indicating that too many SPIs are
+ * currently allocated for this user
+ * @throws {@link #SpiUnavailableException} indicating that the requested SPI could not be
+ * reserved
*/
public SecurityParameterIndex allocateSecurityParameterIndex(
- int direction, InetAddress remoteAddress, int requestedSpi)
+ InetAddress destinationAddress, int requestedSpi)
throws SpiUnavailableException, ResourceUnavailableException {
if (requestedSpi == IpSecManager.INVALID_SECURITY_PARAMETER_INDEX) {
throw new IllegalArgumentException("Requested SPI must be a valid (non-zero) SPI");
}
- return new SecurityParameterIndex(mService, direction, remoteAddress, requestedSpi);
+ return new SecurityParameterIndex(mService, destinationAddress, requestedSpi);
}
/**
@@ -268,14 +289,14 @@
*
* <p>This applies transport mode encapsulation to the given socket. Once applied, I/O on the
* socket will be encapsulated according to the parameters of the {@code IpSecTransform}. When
- * the transform is removed from the socket by calling {@link #removeTransportModeTransform},
+ * the transform is removed from the socket by calling {@link #removeTransportModeTransforms},
* unprotected traffic can resume on that socket.
*
* <p>For security reasons, the destination address of any traffic on the socket must match the
* remote {@code InetAddress} of the {@code IpSecTransform}. Attempts to send traffic to any
* other IP address will result in an IOException. In addition, reads and writes on the socket
* will throw IOException if the user deactivates the transform (by calling {@link
- * IpSecTransform#close()}) without calling {@link #removeTransportModeTransform}.
+ * IpSecTransform#close()}) without calling {@link #removeTransportModeTransforms}.
*
* <h4>Rekey Procedure</h4>
*
@@ -286,15 +307,14 @@
* in-flight packets have been received.
*
* @param socket a stream socket
+ * @param direction the policy direction either {@link #DIRECTION_IN} or {@link #DIRECTION_OUT}
* @param transform a transport mode {@code IpSecTransform}
* @throws IOException indicating that the transform could not be applied
- * @hide
*/
- public void applyTransportModeTransform(Socket socket, IpSecTransform transform)
+ public void applyTransportModeTransform(
+ Socket socket, int direction, IpSecTransform transform)
throws IOException {
- try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket)) {
- applyTransportModeTransform(pfd, transform);
- }
+ applyTransportModeTransform(socket.getFileDescriptor$(), direction, transform);
}
/**
@@ -302,14 +322,14 @@
*
* <p>This applies transport mode encapsulation to the given socket. Once applied, I/O on the
* socket will be encapsulated according to the parameters of the {@code IpSecTransform}. When
- * the transform is removed from the socket by calling {@link #removeTransportModeTransform},
+ * the transform is removed from the socket by calling {@link #removeTransportModeTransforms},
* unprotected traffic can resume on that socket.
*
* <p>For security reasons, the destination address of any traffic on the socket must match the
* remote {@code InetAddress} of the {@code IpSecTransform}. Attempts to send traffic to any
* other IP address will result in an IOException. In addition, reads and writes on the socket
* will throw IOException if the user deactivates the transform (by calling {@link
- * IpSecTransform#close()}) without calling {@link #removeTransportModeTransform}.
+ * IpSecTransform#close()}) without calling {@link #removeTransportModeTransforms}.
*
* <h4>Rekey Procedure</h4>
*
@@ -320,15 +340,13 @@
* in-flight packets have been received.
*
* @param socket a datagram socket
+ * @param direction the policy direction either DIRECTION_IN or DIRECTION_OUT
* @param transform a transport mode {@code IpSecTransform}
* @throws IOException indicating that the transform could not be applied
- * @hide
*/
- public void applyTransportModeTransform(DatagramSocket socket, IpSecTransform transform)
- throws IOException {
- try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromDatagramSocket(socket)) {
- applyTransportModeTransform(pfd, transform);
- }
+ public void applyTransportModeTransform(
+ DatagramSocket socket, int direction, IpSecTransform transform) throws IOException {
+ applyTransportModeTransform(socket.getFileDescriptor$(), direction, transform);
}
/**
@@ -336,14 +354,14 @@
*
* <p>This applies transport mode encapsulation to the given socket. Once applied, I/O on the
* socket will be encapsulated according to the parameters of the {@code IpSecTransform}. When
- * the transform is removed from the socket by calling {@link #removeTransportModeTransform},
+ * the transform is removed from the socket by calling {@link #removeTransportModeTransforms},
* unprotected traffic can resume on that socket.
*
* <p>For security reasons, the destination address of any traffic on the socket must match the
* remote {@code InetAddress} of the {@code IpSecTransform}. Attempts to send traffic to any
* other IP address will result in an IOException. In addition, reads and writes on the socket
* will throw IOException if the user deactivates the transform (by calling {@link
- * IpSecTransform#close()}) without calling {@link #removeTransportModeTransform}.
+ * IpSecTransform#close()}) without calling {@link #removeTransportModeTransforms}.
*
* <h4>Rekey Procedure</h4>
*
@@ -354,24 +372,17 @@
* in-flight packets have been received.
*
* @param socket a socket file descriptor
+ * @param direction the policy direction either DIRECTION_IN or DIRECTION_OUT
* @param transform a transport mode {@code IpSecTransform}
* @throws IOException indicating that the transform could not be applied
*/
- public void applyTransportModeTransform(FileDescriptor socket, IpSecTransform transform)
+ public void applyTransportModeTransform(
+ FileDescriptor socket, int direction, IpSecTransform transform)
throws IOException {
// We dup() the FileDescriptor here because if we don't, then the ParcelFileDescriptor()
- // constructor takes control and closes the user's FD when we exit the method
- // This is behaviorally the same as the other versions, but the PFD constructor does not
- // dup() automatically, whereas PFD.fromSocket() and PDF.fromDatagramSocket() do dup().
+ // constructor takes control and closes the user's FD when we exit the method.
try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(socket)) {
- applyTransportModeTransform(pfd, transform);
- }
- }
-
- /* Call down to activate a transform */
- private void applyTransportModeTransform(ParcelFileDescriptor pfd, IpSecTransform transform) {
- try {
- mService.applyTransportModeTransform(pfd, transform.getResourceId());
+ mService.applyTransportModeTransform(pfd, direction, transform.getResourceId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -395,75 +406,56 @@
/**
* Remove an IPsec transform from a stream socket.
*
- * <p>Once removed, traffic on the socket will not be encrypted. This operation will succeed
- * regardless of the state of the transform. Removing a transform from a socket allows the
- * socket to be reused for communication in the clear.
+ * <p>Once removed, traffic on the socket will not be encrypted. Removing transforms from a
+ * socket allows the socket to be reused for communication in the clear.
*
* <p>If an {@code IpSecTransform} object applied to this socket was deallocated by calling
* {@link IpSecTransform#close()}, then communication on the socket will fail until this method
* is called.
*
* @param socket a socket that previously had a transform applied to it
- * @param transform the IPsec Transform that was previously applied to the given socket
* @throws IOException indicating that the transform could not be removed from the socket
- * @hide
*/
- public void removeTransportModeTransform(Socket socket, IpSecTransform transform)
+ public void removeTransportModeTransforms(Socket socket)
throws IOException {
- try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket)) {
- removeTransportModeTransform(pfd, transform);
- }
+ removeTransportModeTransforms(socket.getFileDescriptor$());
}
/**
* Remove an IPsec transform from a datagram socket.
*
- * <p>Once removed, traffic on the socket will not be encrypted. This operation will succeed
- * regardless of the state of the transform. Removing a transform from a socket allows the
- * socket to be reused for communication in the clear.
+ * <p>Once removed, traffic on the socket will not be encrypted. Removing transforms from a
+ * socket allows the socket to be reused for communication in the clear.
*
* <p>If an {@code IpSecTransform} object applied to this socket was deallocated by calling
* {@link IpSecTransform#close()}, then communication on the socket will fail until this method
* is called.
*
* @param socket a socket that previously had a transform applied to it
- * @param transform the IPsec Transform that was previously applied to the given socket
* @throws IOException indicating that the transform could not be removed from the socket
- * @hide
*/
- public void removeTransportModeTransform(DatagramSocket socket, IpSecTransform transform)
+ public void removeTransportModeTransforms(DatagramSocket socket)
throws IOException {
- try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromDatagramSocket(socket)) {
- removeTransportModeTransform(pfd, transform);
- }
+ removeTransportModeTransforms(socket.getFileDescriptor$());
}
/**
* Remove an IPsec transform from a socket.
*
- * <p>Once removed, traffic on the socket will not be encrypted. This operation will succeed
- * regardless of the state of the transform. Removing a transform from a socket allows the
- * socket to be reused for communication in the clear.
+ * <p>Once removed, traffic on the socket will not be encrypted. Removing transforms from a
+ * socket allows the socket to be reused for communication in the clear.
*
* <p>If an {@code IpSecTransform} object applied to this socket was deallocated by calling
* {@link IpSecTransform#close()}, then communication on the socket will fail until this method
* is called.
*
* @param socket a socket that previously had a transform applied to it
- * @param transform the IPsec Transform that was previously applied to the given socket
* @throws IOException indicating that the transform could not be removed from the socket
*/
- public void removeTransportModeTransform(FileDescriptor socket, IpSecTransform transform)
+ public void removeTransportModeTransforms(FileDescriptor socket)
throws IOException {
try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(socket)) {
- removeTransportModeTransform(pfd, transform);
- }
- }
-
- /* Call down to remove a transform */
- private void removeTransportModeTransform(ParcelFileDescriptor pfd, IpSecTransform transform) {
- try {
- mService.removeTransportModeTransform(pfd, transform.getResourceId());
+ mService.removeTransportModeTransforms(pfd);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -501,7 +493,7 @@
public static final class UdpEncapsulationSocket implements AutoCloseable {
private final ParcelFileDescriptor mPfd;
private final IIpSecService mService;
- private final int mResourceId;
+ private int mResourceId = INVALID_RESOURCE_ID;
private final int mPort;
private final CloseGuard mCloseGuard = CloseGuard.get();
@@ -554,6 +546,7 @@
public void close() throws IOException {
try {
mService.closeUdpEncapsulationSocket(mResourceId);
+ mResourceId = INVALID_RESOURCE_ID;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -633,6 +626,170 @@
}
/**
+ * This class represents an IpSecTunnelInterface
+ *
+ * <p>IpSecTunnelInterface objects track tunnel interfaces that serve as
+ * local endpoints for IPsec tunnels.
+ *
+ * <p>Creating an IpSecTunnelInterface creates a device to which IpSecTransforms may be
+ * applied to provide IPsec security to packets sent through the tunnel. While a tunnel
+ * cannot be used in standalone mode within Android, the higher layers may use the tunnel
+ * to create Network objects which are accessible to the Android system.
+ * @hide
+ */
+ @SystemApi
+ public static final class IpSecTunnelInterface implements AutoCloseable {
+ private final IIpSecService mService;
+ private final InetAddress mRemoteAddress;
+ private final InetAddress mLocalAddress;
+ private final Network mUnderlyingNetwork;
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+ private String mInterfaceName;
+ private int mResourceId = INVALID_RESOURCE_ID;
+
+ /** Get the underlying SPI held by this object. */
+ public String getInterfaceName() {
+ return mInterfaceName;
+ }
+
+ /**
+ * Add an address to the IpSecTunnelInterface
+ *
+ * <p>Add an address which may be used as the local inner address for
+ * tunneled traffic.
+ *
+ * @param address the local address for traffic inside the tunnel
+ * @throws IOException if the address could not be added
+ * @hide
+ */
+ public void addAddress(LinkAddress address) throws IOException {
+ }
+
+ /**
+ * Remove an address from the IpSecTunnelInterface
+ *
+ * <p>Remove an address which was previously added to the IpSecTunnelInterface
+ *
+ * @param address to be removed
+ * @throws IOException if the address could not be removed
+ * @hide
+ */
+ public void removeAddress(LinkAddress address) throws IOException {
+ }
+
+ private IpSecTunnelInterface(@NonNull IIpSecService service,
+ @NonNull InetAddress localAddress, @NonNull InetAddress remoteAddress,
+ @NonNull Network underlyingNetwork)
+ throws ResourceUnavailableException, IOException {
+ mService = service;
+ mLocalAddress = localAddress;
+ mRemoteAddress = remoteAddress;
+ mUnderlyingNetwork = underlyingNetwork;
+
+ try {
+ IpSecTunnelInterfaceResponse result =
+ mService.createTunnelInterface(
+ localAddress.getHostAddress(),
+ remoteAddress.getHostAddress(),
+ underlyingNetwork,
+ new Binder());
+ switch (result.status) {
+ case Status.OK:
+ break;
+ case Status.RESOURCE_UNAVAILABLE:
+ throw new ResourceUnavailableException(
+ "No more tunnel interfaces may be allocated by this requester.");
+ default:
+ throw new RuntimeException(
+ "Unknown status returned by IpSecService: " + result.status);
+ }
+ mResourceId = result.resourceId;
+ mInterfaceName = result.interfaceName;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mCloseGuard.open("constructor");
+ }
+
+ /**
+ * Delete an IpSecTunnelInterface
+ *
+ * <p>Calling close will deallocate the IpSecTunnelInterface and all of its system
+ * resources. Any packets bound for this interface either inbound or outbound will
+ * all be lost.
+ */
+ @Override
+ public void close() {
+ try {
+ mService.deleteTunnelInterface(mResourceId);
+ mResourceId = INVALID_RESOURCE_ID;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mCloseGuard.close();
+ }
+
+ /** Check that the Interface was closed properly. */
+ @Override
+ protected void finalize() throws Throwable {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ close();
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ public int getResourceId() {
+ return mResourceId;
+ }
+ }
+
+ /**
+ * Create a new IpSecTunnelInterface as a local endpoint for tunneled IPsec traffic.
+ *
+ * <p>An application that creates tunnels is responsible for cleaning up the tunnel when the
+ * underlying network goes away, and the onLost() callback is received.
+ *
+ * @param localAddress The local addres of the tunnel
+ * @param remoteAddress The local addres of the tunnel
+ * @param underlyingNetwork the {@link Network} that will carry traffic for this tunnel.
+ * This network should almost certainly be a network such as WiFi with an L2 address.
+ * @return a new {@link IpSecManager#IpSecTunnelInterface} with the specified properties
+ * @throws IOException indicating that the socket could not be opened or bound
+ * @throws ResourceUnavailableException indicating that too many encapsulation sockets are open
+ * @hide
+ */
+ @SystemApi
+ public IpSecTunnelInterface createIpSecTunnelInterface(@NonNull InetAddress localAddress,
+ @NonNull InetAddress remoteAddress, @NonNull Network underlyingNetwork)
+ throws ResourceUnavailableException, IOException {
+ return new IpSecTunnelInterface(mService, localAddress, remoteAddress, underlyingNetwork);
+ }
+
+ /**
+ * Apply a transform to the IpSecTunnelInterface
+ *
+ * @param tunnel The {@link IpSecManager#IpSecTunnelInterface} that will use the supplied
+ * transform.
+ * @param direction the direction, {@link DIRECTION_OUT} or {@link #DIRECTION_IN} in which
+ * the transform will be used.
+ * @param transform an {@link IpSecTransform} created in tunnel mode
+ * @throws IOException indicating that the transform could not be applied due to a lower
+ * layer failure.
+ * @hide
+ */
+ @SystemApi
+ public void applyTunnelModeTransform(IpSecTunnelInterface tunnel, int direction,
+ IpSecTransform transform) throws IOException {
+ try {
+ mService.applyTunnelModeTransform(
+ tunnel.getResourceId(), direction, transform.getResourceId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ /**
* Construct an instance of IpSecManager within an application context.
*
* @param context the application context for this manager
diff --git a/android/net/IpSecTransform.java b/android/net/IpSecTransform.java
index 7cd742b..37e2c4f 100644
--- a/android/net/IpSecTransform.java
+++ b/android/net/IpSecTransform.java
@@ -38,13 +38,11 @@
import java.net.InetAddress;
/**
- * This class represents an IPsec transform, which comprises security associations in one or both
- * directions.
+ * This class represents a transform, which roughly corresponds to an IPsec Security Association.
*
* <p>Transforms are created using {@link IpSecTransform.Builder}. Each {@code IpSecTransform}
- * object encapsulates the properties and state of an inbound and outbound IPsec security
- * association. That includes, but is not limited to, algorithm choice, key material, and allocated
- * system resources.
+ * object encapsulates the properties and state of an IPsec security association. That includes,
+ * but is not limited to, algorithm choice, key material, and allocated system resources.
*
* @see <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the
* Internet Protocol</a>
@@ -52,23 +50,6 @@
public final class IpSecTransform implements AutoCloseable {
private static final String TAG = "IpSecTransform";
- /**
- * For direction-specific attributes of an {@link IpSecTransform}, indicates that an attribute
- * applies to traffic towards the host.
- */
- public static final int DIRECTION_IN = 0;
-
- /**
- * For direction-specific attributes of an {@link IpSecTransform}, indicates that an attribute
- * applies to traffic from the host.
- */
- public static final int DIRECTION_OUT = 1;
-
- /** @hide */
- @IntDef(value = {DIRECTION_IN, DIRECTION_OUT})
- @Retention(RetentionPolicy.SOURCE)
- public @interface TransformDirection {}
-
/** @hide */
public static final int MODE_TRANSPORT = 0;
@@ -143,8 +124,7 @@
synchronized (this) {
try {
IIpSecService svc = getIpSecService();
- IpSecTransformResponse result =
- svc.createTransportModeTransform(mConfig, new Binder());
+ IpSecTransformResponse result = svc.createTransform(mConfig, new Binder());
int status = result.status;
checkResultStatus(status);
mResourceId = result.resourceId;
@@ -170,7 +150,7 @@
*
* <p>Deactivating a transform while it is still applied to a socket will result in errors on
* that socket. Make sure to remove transforms by calling {@link
- * IpSecManager#removeTransportModeTransform}. Note, removing an {@code IpSecTransform} from a
+ * IpSecManager#removeTransportModeTransforms}. Note, removing an {@code IpSecTransform} from a
* socket will not deactivate it (because one transform may be applied to multiple sockets).
*
* <p>It is safe to call this method on a transform that has already been deactivated.
@@ -189,7 +169,7 @@
* still want to clear out the transform.
*/
IIpSecService svc = getIpSecService();
- svc.deleteTransportModeTransform(mResourceId);
+ svc.deleteTransform(mResourceId);
stopKeepalive();
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
@@ -272,96 +252,49 @@
private IpSecConfig mConfig;
/**
- * Set the encryption algorithm for the given direction.
- *
- * <p>If encryption is set for a direction without also providing an SPI for that direction,
- * creation of an {@code IpSecTransform} will fail when attempting to build the transform.
+ * Set the encryption algorithm.
*
* <p>Encryption is mutually exclusive with authenticated encryption.
*
- * @param direction either {@link #DIRECTION_IN} or {@link #DIRECTION_OUT}
* @param algo {@link IpSecAlgorithm} specifying the encryption to be applied.
*/
- public IpSecTransform.Builder setEncryption(
- @TransformDirection int direction, IpSecAlgorithm algo) {
+ public IpSecTransform.Builder setEncryption(@NonNull IpSecAlgorithm algo) {
// TODO: throw IllegalArgumentException if algo is not an encryption algorithm.
- mConfig.setEncryption(direction, algo);
+ Preconditions.checkNotNull(algo);
+ mConfig.setEncryption(algo);
return this;
}
/**
- * Set the authentication (integrity) algorithm for the given direction.
- *
- * <p>If authentication is set for a direction without also providing an SPI for that
- * direction, creation of an {@code IpSecTransform} will fail when attempting to build the
- * transform.
+ * Set the authentication (integrity) algorithm.
*
* <p>Authentication is mutually exclusive with authenticated encryption.
*
- * @param direction either {@link #DIRECTION_IN} or {@link #DIRECTION_OUT}
* @param algo {@link IpSecAlgorithm} specifying the authentication to be applied.
*/
- public IpSecTransform.Builder setAuthentication(
- @TransformDirection int direction, IpSecAlgorithm algo) {
+ public IpSecTransform.Builder setAuthentication(@NonNull IpSecAlgorithm algo) {
// TODO: throw IllegalArgumentException if algo is not an authentication algorithm.
- mConfig.setAuthentication(direction, algo);
+ Preconditions.checkNotNull(algo);
+ mConfig.setAuthentication(algo);
return this;
}
/**
- * Set the authenticated encryption algorithm for the given direction.
+ * Set the authenticated encryption algorithm.
*
- * <p>If an authenticated encryption algorithm is set for a given direction without also
- * providing an SPI for that direction, creation of an {@code IpSecTransform} will fail when
- * attempting to build the transform.
- *
- * <p>The Authenticated Encryption (AE) class of algorithms are also known as Authenticated
- * Encryption with Associated Data (AEAD) algorithms, or Combined mode algorithms (as
- * referred to in <a href="https://tools.ietf.org/html/rfc4301">RFC 4301</a>).
+ * <p>The Authenticated Encryption (AE) class of algorithms are also known as
+ * Authenticated Encryption with Associated Data (AEAD) algorithms, or Combined mode
+ * algorithms (as referred to in
+ * <a href="https://tools.ietf.org/html/rfc4301">RFC 4301</a>).
*
* <p>Authenticated encryption is mutually exclusive with encryption and authentication.
*
- * @param direction either {@link #DIRECTION_IN} or {@link #DIRECTION_OUT}
* @param algo {@link IpSecAlgorithm} specifying the authenticated encryption algorithm to
* be applied.
*/
- public IpSecTransform.Builder setAuthenticatedEncryption(
- @TransformDirection int direction, IpSecAlgorithm algo) {
- mConfig.setAuthenticatedEncryption(direction, algo);
- return this;
- }
-
- /**
- * Set the SPI for the given direction.
- *
- * <p>Because IPsec operates at the IP layer, this 32-bit identifier uniquely identifies
- * packets to a given destination address. To prevent SPI collisions, values should be
- * reserved by calling {@link IpSecManager#allocateSecurityParameterIndex}.
- *
- * <p>If the SPI and algorithms are omitted for one direction, traffic in that direction
- * will not be encrypted or authenticated.
- *
- * @param direction either {@link #DIRECTION_IN} or {@link #DIRECTION_OUT}
- * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed
- * traffic
- */
- public IpSecTransform.Builder setSpi(
- @TransformDirection int direction, IpSecManager.SecurityParameterIndex spi) {
- mConfig.setSpiResourceId(direction, spi.getResourceId());
- return this;
- }
-
- /**
- * Set the {@link Network} which will carry tunneled traffic.
- *
- * <p>Restricts the transformed traffic to a particular {@link Network}. This is required
- * for tunnel mode, otherwise tunneled traffic would be sent on the default network.
- *
- * @hide
- */
- @SystemApi
- public IpSecTransform.Builder setUnderlyingNetwork(Network net) {
- mConfig.setNetwork(net);
+ public IpSecTransform.Builder setAuthenticatedEncryption(@NonNull IpSecAlgorithm algo) {
+ Preconditions.checkNotNull(algo);
+ mConfig.setAuthenticatedEncryption(algo);
return this;
}
@@ -379,8 +312,12 @@
* encapsulated traffic. In the case of IKEv2, this should be port 4500.
*/
public IpSecTransform.Builder setIpv4Encapsulation(
- IpSecManager.UdpEncapsulationSocket localSocket, int remotePort) {
+ @NonNull IpSecManager.UdpEncapsulationSocket localSocket, int remotePort) {
+ Preconditions.checkNotNull(localSocket);
mConfig.setEncapType(ENCAP_ESPINUDP);
+ if (localSocket.getResourceId() == INVALID_RESOURCE_ID) {
+ throw new IllegalArgumentException("Invalid UdpEncapsulationSocket");
+ }
mConfig.setEncapSocketResourceId(localSocket.getResourceId());
mConfig.setEncapRemotePort(remotePort);
return this;
@@ -413,21 +350,33 @@
* will not affect any network traffic until it has been applied to one or more sockets.
*
* @see IpSecManager#applyTransportModeTransform
- * @param remoteAddress the remote {@code InetAddress} of traffic on sockets that will use
- * this transform
+ * @param sourceAddress the source {@code InetAddress} of traffic on sockets that will use
+ * this transform; this address must belong to the Network used by all sockets that
+ * utilize this transform; if provided, then only traffic originating from the
+ * specified source address will be processed.
+ * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed
+ * traffic
* @throws IllegalArgumentException indicating that a particular combination of transform
* properties is invalid
- * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms are
- * active
+ * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms
+ * are active
* @throws IpSecManager.SpiUnavailableException indicating the rare case where an SPI
* collides with an existing transform
* @throws IOException indicating other errors
*/
- public IpSecTransform buildTransportModeTransform(InetAddress remoteAddress)
+ public IpSecTransform buildTransportModeTransform(
+ @NonNull InetAddress sourceAddress,
+ @NonNull IpSecManager.SecurityParameterIndex spi)
throws IpSecManager.ResourceUnavailableException,
IpSecManager.SpiUnavailableException, IOException {
+ Preconditions.checkNotNull(sourceAddress);
+ Preconditions.checkNotNull(spi);
+ if (spi.getResourceId() == INVALID_RESOURCE_ID) {
+ throw new IllegalArgumentException("Invalid SecurityParameterIndex");
+ }
mConfig.setMode(MODE_TRANSPORT);
- mConfig.setRemoteAddress(remoteAddress.getHostAddress());
+ mConfig.setSourceAddress(sourceAddress.getHostAddress());
+ mConfig.setSpiResourceId(spi.getResourceId());
// FIXME: modifying a builder after calling build can change the built transform.
return new IpSecTransform(mContext, mConfig).activate();
}
@@ -436,22 +385,34 @@
* Build and return an {@link IpSecTransform} object as a Tunnel Mode Transform. Some
* parameters have interdependencies that are checked at build time.
*
- * @param localAddress the {@link InetAddress} that provides the local endpoint for this
+ * @param sourceAddress the {@link InetAddress} that provides the source address for this
* IPsec tunnel. This is almost certainly an address belonging to the {@link Network}
* that will originate the traffic, which is set as the {@link #setUnderlyingNetwork}.
- * @param remoteAddress the {@link InetAddress} representing the remote endpoint of this
- * IPsec tunnel.
+ * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed
+ * traffic
* @throws IllegalArgumentException indicating that a particular combination of transform
* properties is invalid.
+ * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms
+ * are active
+ * @throws IpSecManager.SpiUnavailableException indicating the rare case where an SPI
+ * collides with an existing transform
+ * @throws IOException indicating other errors
* @hide
*/
+ @SystemApi
public IpSecTransform buildTunnelModeTransform(
- InetAddress localAddress, InetAddress remoteAddress) {
- // FIXME: argument validation here
- // throw new IllegalArgumentException("Natt Keepalive requires UDP Encapsulation");
- mConfig.setLocalAddress(localAddress.getHostAddress());
- mConfig.setRemoteAddress(remoteAddress.getHostAddress());
+ @NonNull InetAddress sourceAddress,
+ @NonNull IpSecManager.SecurityParameterIndex spi)
+ throws IpSecManager.ResourceUnavailableException,
+ IpSecManager.SpiUnavailableException, IOException {
+ Preconditions.checkNotNull(sourceAddress);
+ Preconditions.checkNotNull(spi);
+ if (spi.getResourceId() == INVALID_RESOURCE_ID) {
+ throw new IllegalArgumentException("Invalid SecurityParameterIndex");
+ }
mConfig.setMode(MODE_TUNNEL);
+ mConfig.setSourceAddress(sourceAddress.getHostAddress());
+ mConfig.setSpiResourceId(spi.getResourceId());
return new IpSecTransform(mContext, mConfig);
}
diff --git a/android/net/IpSecTunnelInterfaceResponse.java b/android/net/IpSecTunnelInterfaceResponse.java
new file mode 100644
index 0000000..c23d831
--- /dev/null
+++ b/android/net/IpSecTunnelInterfaceResponse.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2018 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.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class is used to return an IpSecTunnelInterface resource Id and and corresponding status
+ * from the IpSecService to an IpSecTunnelInterface object.
+ *
+ * @hide
+ */
+public final class IpSecTunnelInterfaceResponse implements Parcelable {
+ private static final String TAG = "IpSecTunnelInterfaceResponse";
+
+ public final int resourceId;
+ public final String interfaceName;
+ public final int status;
+ // Parcelable Methods
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(status);
+ out.writeInt(resourceId);
+ out.writeString(interfaceName);
+ }
+
+ public IpSecTunnelInterfaceResponse(int inStatus) {
+ if (inStatus == IpSecManager.Status.OK) {
+ throw new IllegalArgumentException("Valid status implies other args must be provided");
+ }
+ status = inStatus;
+ resourceId = IpSecManager.INVALID_RESOURCE_ID;
+ interfaceName = "";
+ }
+
+ public IpSecTunnelInterfaceResponse(int inStatus, int inResourceId, String inInterfaceName) {
+ status = inStatus;
+ resourceId = inResourceId;
+ interfaceName = inInterfaceName;
+ }
+
+ private IpSecTunnelInterfaceResponse(Parcel in) {
+ status = in.readInt();
+ resourceId = in.readInt();
+ interfaceName = in.readString();
+ }
+
+ public static final Parcelable.Creator<IpSecTunnelInterfaceResponse> CREATOR =
+ new Parcelable.Creator<IpSecTunnelInterfaceResponse>() {
+ public IpSecTunnelInterfaceResponse createFromParcel(Parcel in) {
+ return new IpSecTunnelInterfaceResponse(in);
+ }
+
+ public IpSecTunnelInterfaceResponse[] newArray(int size) {
+ return new IpSecTunnelInterfaceResponse[size];
+ }
+ };
+}
diff --git a/android/net/KeepalivePacketData.java b/android/net/KeepalivePacketData.java
new file mode 100644
index 0000000..08d4ff5
--- /dev/null
+++ b/android/net/KeepalivePacketData.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.system.OsConstants;
+import android.net.ConnectivityManager;
+import android.net.util.IpUtils;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.system.OsConstants;
+import android.util.Log;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import static android.net.ConnectivityManager.PacketKeepalive.*;
+
+/**
+ * Represents the actual packets that are sent by the
+ * {@link android.net.ConnectivityManager.PacketKeepalive} API.
+ *
+ * @hide
+ */
+public class KeepalivePacketData implements Parcelable {
+ private static final String TAG = "KeepalivePacketData";
+
+ /** Source IP address */
+ public final InetAddress srcAddress;
+
+ /** Destination IP address */
+ public final InetAddress dstAddress;
+
+ /** Source port */
+ public final int srcPort;
+
+ /** Destination port */
+ public final int dstPort;
+
+ /** Packet data. A raw byte string of packet data, not including the link-layer header. */
+ private final byte[] mPacket;
+
+ private static final int IPV4_HEADER_LENGTH = 20;
+ private static final int UDP_HEADER_LENGTH = 8;
+
+ // This should only be constructed via static factory methods, such as
+ // nattKeepalivePacket
+ protected KeepalivePacketData(InetAddress srcAddress, int srcPort,
+ InetAddress dstAddress, int dstPort, byte[] data) throws InvalidPacketException {
+ this.srcAddress = srcAddress;
+ this.dstAddress = dstAddress;
+ this.srcPort = srcPort;
+ this.dstPort = dstPort;
+ this.mPacket = data;
+
+ // Check we have two IP addresses of the same family.
+ if (srcAddress == null || dstAddress == null || !srcAddress.getClass().getName()
+ .equals(dstAddress.getClass().getName())) {
+ Log.e(TAG, "Invalid or mismatched InetAddresses in KeepalivePacketData");
+ throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+ }
+
+ // Check the ports.
+ if (!IpUtils.isValidUdpOrTcpPort(srcPort) || !IpUtils.isValidUdpOrTcpPort(dstPort)) {
+ Log.e(TAG, "Invalid ports in KeepalivePacketData");
+ throw new InvalidPacketException(ERROR_INVALID_PORT);
+ }
+ }
+
+ public static class InvalidPacketException extends Exception {
+ public final int error;
+ public InvalidPacketException(int error) {
+ this.error = error;
+ }
+ }
+
+ public byte[] getPacket() {
+ return mPacket.clone();
+ }
+
+ public static KeepalivePacketData nattKeepalivePacket(
+ InetAddress srcAddress, int srcPort, InetAddress dstAddress, int dstPort)
+ throws InvalidPacketException {
+
+ // FIXME: remove this and actually support IPv6 keepalives
+ if (srcAddress instanceof Inet6Address && dstAddress instanceof Inet6Address) {
+ // Optimistically returning an IPv6 Keepalive Packet with no data,
+ // which currently only works on cellular
+ return new KeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, new byte[0]);
+ }
+
+ if (!(srcAddress instanceof Inet4Address) || !(dstAddress instanceof Inet4Address)) {
+ throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+ }
+
+ if (dstPort != NATT_PORT) {
+ throw new InvalidPacketException(ERROR_INVALID_PORT);
+ }
+
+ int length = IPV4_HEADER_LENGTH + UDP_HEADER_LENGTH + 1;
+ ByteBuffer buf = ByteBuffer.allocate(length);
+ buf.order(ByteOrder.BIG_ENDIAN);
+ buf.putShort((short) 0x4500); // IP version and TOS
+ buf.putShort((short) length);
+ buf.putInt(0); // ID, flags, offset
+ buf.put((byte) 64); // TTL
+ buf.put((byte) OsConstants.IPPROTO_UDP);
+ int ipChecksumOffset = buf.position();
+ buf.putShort((short) 0); // IP checksum
+ buf.put(srcAddress.getAddress());
+ buf.put(dstAddress.getAddress());
+ buf.putShort((short) srcPort);
+ buf.putShort((short) dstPort);
+ buf.putShort((short) (length - 20)); // UDP length
+ int udpChecksumOffset = buf.position();
+ buf.putShort((short) 0); // UDP checksum
+ buf.put((byte) 0xff); // NAT-T keepalive
+ buf.putShort(ipChecksumOffset, IpUtils.ipChecksum(buf, 0));
+ buf.putShort(udpChecksumOffset, IpUtils.udpChecksum(buf, 0, IPV4_HEADER_LENGTH));
+
+ return new KeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, buf.array());
+ }
+
+ /* Parcelable Implementation */
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Write to parcel */
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(srcAddress.getHostAddress());
+ out.writeString(dstAddress.getHostAddress());
+ out.writeInt(srcPort);
+ out.writeInt(dstPort);
+ out.writeByteArray(mPacket);
+ }
+
+ private KeepalivePacketData(Parcel in) {
+ srcAddress = NetworkUtils.numericToInetAddress(in.readString());
+ dstAddress = NetworkUtils.numericToInetAddress(in.readString());
+ srcPort = in.readInt();
+ dstPort = in.readInt();
+ mPacket = in.createByteArray();
+ }
+
+ /** Parcelable Creator */
+ public static final Parcelable.Creator<KeepalivePacketData> CREATOR =
+ new Parcelable.Creator<KeepalivePacketData>() {
+ public KeepalivePacketData createFromParcel(Parcel in) {
+ return new KeepalivePacketData(in);
+ }
+
+ public KeepalivePacketData[] newArray(int size) {
+ return new KeepalivePacketData[size];
+ }
+ };
+
+}
diff --git a/android/net/LinkProperties.java b/android/net/LinkProperties.java
index 4e474c8..f525b1f 100644
--- a/android/net/LinkProperties.java
+++ b/android/net/LinkProperties.java
@@ -50,6 +50,8 @@
private String mIfaceName;
private ArrayList<LinkAddress> mLinkAddresses = new ArrayList<LinkAddress>();
private ArrayList<InetAddress> mDnses = new ArrayList<InetAddress>();
+ private boolean mUsePrivateDns;
+ private String mPrivateDnsServerName;
private String mDomains;
private ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
private ProxyInfo mHttpProxy;
@@ -165,6 +167,8 @@
mIfaceName = source.getInterfaceName();
for (LinkAddress l : source.getLinkAddresses()) mLinkAddresses.add(l);
for (InetAddress i : source.getDnsServers()) mDnses.add(i);
+ mUsePrivateDns = source.mUsePrivateDns;
+ mPrivateDnsServerName = source.mPrivateDnsServerName;
mDomains = source.getDomains();
for (RouteInfo r : source.getRoutes()) mRoutes.add(r);
mHttpProxy = (source.getHttpProxy() == null) ?
@@ -391,6 +395,59 @@
}
/**
+ * Set whether private DNS is currently in use on this network.
+ *
+ * @param usePrivateDns The private DNS state.
+ * @hide
+ */
+ public void setUsePrivateDns(boolean usePrivateDns) {
+ mUsePrivateDns = usePrivateDns;
+ }
+
+ /**
+ * Returns whether private DNS is currently in use on this network. When
+ * private DNS is in use, applications must not send unencrypted DNS
+ * queries as doing so could reveal private user information. Furthermore,
+ * if private DNS is in use and {@link #getPrivateDnsServerName} is not
+ * {@code null}, DNS queries must be sent to the specified DNS server.
+ *
+ * @return {@code true} if private DNS is in use, {@code false} otherwise.
+ */
+ public boolean isPrivateDnsActive() {
+ return mUsePrivateDns;
+ }
+
+ /**
+ * Set the name of the private DNS server to which private DNS queries
+ * should be sent when in strict mode. This value should be {@code null}
+ * when private DNS is off or in opportunistic mode.
+ *
+ * @param privateDnsServerName The private DNS server name.
+ * @hide
+ */
+ public void setPrivateDnsServerName(@Nullable String privateDnsServerName) {
+ mPrivateDnsServerName = privateDnsServerName;
+ }
+
+ /**
+ * Returns the private DNS server name that is in use. If not {@code null},
+ * private DNS is in strict mode. In this mode, applications should ensure
+ * that all DNS queries are encrypted and sent to this hostname and that
+ * queries are only sent if the hostname's certificate is valid. If
+ * {@code null} and {@link #isPrivateDnsActive} is {@code true}, private
+ * DNS is in opportunistic mode, and applications should ensure that DNS
+ * queries are encrypted and sent to a DNS server returned by
+ * {@link #getDnsServers}. System DNS will handle each of these cases
+ * correctly, but applications implementing their own DNS lookups must make
+ * sure to follow these requirements.
+ *
+ * @return The private DNS server name.
+ */
+ public @Nullable String getPrivateDnsServerName() {
+ return mPrivateDnsServerName;
+ }
+
+ /**
* Sets the DNS domain search path used on this link.
*
* @param domains A {@link String} listing in priority order the comma separated
@@ -622,6 +679,8 @@
mIfaceName = null;
mLinkAddresses.clear();
mDnses.clear();
+ mUsePrivateDns = false;
+ mPrivateDnsServerName = null;
mDomains = null;
mRoutes.clear();
mHttpProxy = null;
@@ -649,6 +708,13 @@
for (InetAddress addr : mDnses) dns += addr.getHostAddress() + ",";
dns += "] ";
+ String usePrivateDns = "UsePrivateDns: " + mUsePrivateDns + " ";
+
+ String privateDnsServerName = "";
+ if (privateDnsServerName != null) {
+ privateDnsServerName = "PrivateDnsServerName: " + mPrivateDnsServerName + " ";
+ }
+
String domainName = "Domains: " + mDomains;
String mtu = " MTU: " + mMtu;
@@ -671,8 +737,9 @@
}
stacked += "] ";
}
- return "{" + ifaceName + linkAddresses + routes + dns + domainName + mtu
- + tcpBuffSizes + proxy + stacked + "}";
+ return "{" + ifaceName + linkAddresses + routes + dns + usePrivateDns
+ + privateDnsServerName + domainName + mtu + tcpBuffSizes + proxy
+ + stacked + "}";
}
/**
@@ -896,6 +963,20 @@
}
/**
+ * Compares this {@code LinkProperties} private DNS settings against the
+ * target.
+ *
+ * @param target LinkProperties to compare.
+ * @return {@code true} if both are identical, {@code false} otherwise.
+ * @hide
+ */
+ public boolean isIdenticalPrivateDns(LinkProperties target) {
+ return (isPrivateDnsActive() == target.isPrivateDnsActive()
+ && TextUtils.equals(getPrivateDnsServerName(),
+ target.getPrivateDnsServerName()));
+ }
+
+ /**
* Compares this {@code LinkProperties} Routes against the target
*
* @param target LinkProperties to compare.
@@ -989,14 +1070,15 @@
* stacked interfaces are not so much a property of the link as a
* description of connections between links.
*/
- return isIdenticalInterfaceName(target) &&
- isIdenticalAddresses(target) &&
- isIdenticalDnses(target) &&
- isIdenticalRoutes(target) &&
- isIdenticalHttpProxy(target) &&
- isIdenticalStackedLinks(target) &&
- isIdenticalMtu(target) &&
- isIdenticalTcpBufferSizes(target);
+ return isIdenticalInterfaceName(target)
+ && isIdenticalAddresses(target)
+ && isIdenticalDnses(target)
+ && isIdenticalPrivateDns(target)
+ && isIdenticalRoutes(target)
+ && isIdenticalHttpProxy(target)
+ && isIdenticalStackedLinks(target)
+ && isIdenticalMtu(target)
+ && isIdenticalTcpBufferSizes(target);
}
/**
@@ -1091,7 +1173,9 @@
+ ((null == mHttpProxy) ? 0 : mHttpProxy.hashCode())
+ mStackedLinks.hashCode() * 47)
+ mMtu * 51
- + ((null == mTcpBufferSizes) ? 0 : mTcpBufferSizes.hashCode());
+ + ((null == mTcpBufferSizes) ? 0 : mTcpBufferSizes.hashCode())
+ + (mUsePrivateDns ? 57 : 0)
+ + ((null == mPrivateDnsServerName) ? 0 : mPrivateDnsServerName.hashCode());
}
/**
@@ -1108,6 +1192,8 @@
for(InetAddress d : mDnses) {
dest.writeByteArray(d.getAddress());
}
+ dest.writeBoolean(mUsePrivateDns);
+ dest.writeString(mPrivateDnsServerName);
dest.writeString(mDomains);
dest.writeInt(mMtu);
dest.writeString(mTcpBufferSizes);
@@ -1148,6 +1234,8 @@
netProp.addDnsServer(InetAddress.getByAddress(in.createByteArray()));
} catch (UnknownHostException e) { }
}
+ netProp.setUsePrivateDns(in.readBoolean());
+ netProp.setPrivateDnsServerName(in.readString());
netProp.setDomains(in.readString());
netProp.setMtu(in.readInt());
netProp.setTcpBufferSizes(in.readString());
diff --git a/android/net/MacAddress.java b/android/net/MacAddress.java
index d6992aa..287bdc8 100644
--- a/android/net/MacAddress.java
+++ b/android/net/MacAddress.java
@@ -17,6 +17,7 @@
package android.net;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
@@ -60,7 +61,7 @@
})
public @interface MacAddressType { }
- /** Indicates a MAC address of unknown type. */
+ /** @hide Indicates a MAC address of unknown type. */
public static final int TYPE_UNKNOWN = 0;
/** Indicates a MAC address is a unicast address. */
public static final int TYPE_UNICAST = 1;
@@ -92,7 +93,7 @@
*
* @return the int constant representing the MAC address type of this MacAddress.
*/
- public @MacAddressType int addressType() {
+ public @MacAddressType int getAddressType() {
if (equals(BROADCAST_ADDRESS)) {
return TYPE_BROADCAST;
}
@@ -120,12 +121,12 @@
/**
* @return a byte array representation of this MacAddress.
*/
- public byte[] toByteArray() {
+ public @NonNull byte[] toByteArray() {
return byteAddrFromLongAddr(mAddr);
}
@Override
- public String toString() {
+ public @NonNull String toString() {
return stringAddrFromLongAddr(mAddr);
}
@@ -133,7 +134,7 @@
* @return a String representation of the OUI part of this MacAddress made of 3 hexadecimal
* numbers in [0,ff] joined by ':' characters.
*/
- public String toOuiString() {
+ public @NonNull String toOuiString() {
return String.format(
"%02x:%02x:%02x", (mAddr >> 40) & 0xff, (mAddr >> 32) & 0xff, (mAddr >> 24) & 0xff);
}
@@ -197,7 +198,7 @@
if (!isMacAddress(addr)) {
return TYPE_UNKNOWN;
}
- return MacAddress.fromBytes(addr).addressType();
+ return MacAddress.fromBytes(addr).getAddressType();
}
/**
@@ -211,7 +212,7 @@
*
* @hide
*/
- public static byte[] byteAddrFromStringAddr(String addr) {
+ public static @NonNull byte[] byteAddrFromStringAddr(String addr) {
Preconditions.checkNotNull(addr);
String[] parts = addr.split(":");
if (parts.length != ETHER_ADDR_LEN) {
@@ -239,7 +240,7 @@
*
* @hide
*/
- public static String stringAddrFromByteAddr(byte[] addr) {
+ public static @NonNull String stringAddrFromByteAddr(byte[] addr) {
if (!isMacAddress(addr)) {
return null;
}
@@ -291,7 +292,7 @@
// Internal conversion function equivalent to stringAddrFromByteAddr(byteAddrFromLongAddr(addr))
// that avoids the allocation of an intermediary byte[].
- private static String stringAddrFromLongAddr(long addr) {
+ private static @NonNull String stringAddrFromLongAddr(long addr) {
return String.format("%02x:%02x:%02x:%02x:%02x:%02x",
(addr >> 40) & 0xff,
(addr >> 32) & 0xff,
@@ -310,7 +311,7 @@
* @return the MacAddress corresponding to the given String representation.
* @throws IllegalArgumentException if the given String is not a valid representation.
*/
- public static MacAddress fromString(String addr) {
+ public static @NonNull MacAddress fromString(@NonNull String addr) {
return new MacAddress(longAddrFromStringAddr(addr));
}
@@ -322,7 +323,7 @@
* @return the MacAddress corresponding to the given byte array representation.
* @throws IllegalArgumentException if the given byte array is not a valid representation.
*/
- public static MacAddress fromBytes(byte[] addr) {
+ public static @NonNull MacAddress fromBytes(@NonNull byte[] addr) {
return new MacAddress(longAddrFromByteAddr(addr));
}
@@ -336,7 +337,7 @@
*
* @hide
*/
- public static MacAddress createRandomUnicastAddress() {
+ public static @NonNull MacAddress createRandomUnicastAddress() {
return createRandomUnicastAddress(BASE_GOOGLE_MAC, new Random());
}
@@ -352,7 +353,7 @@
*
* @hide
*/
- public static MacAddress createRandomUnicastAddress(MacAddress base, Random r) {
+ public static @NonNull MacAddress createRandomUnicastAddress(MacAddress base, Random r) {
long addr = (base.mAddr & OUI_MASK) | (NIC_MASK & r.nextLong());
addr = addr | LOCALLY_ASSIGNED_MASK;
addr = addr & ~MULTICAST_MASK;
diff --git a/android/net/Network.java b/android/net/Network.java
index 903b602..5df168d 100644
--- a/android/net/Network.java
+++ b/android/net/Network.java
@@ -21,6 +21,7 @@
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
+import android.util.proto.ProtoOutputStream;
import com.android.okhttp.internalandroidapi.Dns;
import com.android.okhttp.internalandroidapi.HttpURLConnectionFactory;
@@ -356,13 +357,13 @@
// Multiple Provisioning Domains API recommendations, as made by the
// IETF mif working group.
//
- // The HANDLE_MAGIC value MUST be kept in sync with the corresponding
+ // The handleMagic value MUST be kept in sync with the corresponding
// value in the native/android/net.c NDK implementation.
if (netId == 0) {
return 0L; // make this zero condition obvious for debugging
}
- final long HANDLE_MAGIC = 0xfacade;
- return (((long) netId) << 32) | HANDLE_MAGIC;
+ final long handleMagic = 0xcafed00dL;
+ return (((long) netId) << 32) | handleMagic;
}
// implement the Parcelable interface
@@ -402,4 +403,11 @@
public String toString() {
return Integer.toString(netId);
}
+
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(NetworkProto.NET_ID, netId);
+ proto.end(token);
+ }
}
diff --git a/android/net/NetworkAgent.java b/android/net/NetworkAgent.java
index 2dacf8f..52a2354 100644
--- a/android/net/NetworkAgent.java
+++ b/android/net/NetworkAgent.java
@@ -17,6 +17,7 @@
package android.net;
import android.content.Context;
+import android.net.ConnectivityManager.PacketKeepalive;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -26,7 +27,6 @@
import com.android.internal.util.AsyncChannel;
import com.android.internal.util.Protocol;
-import android.net.ConnectivityManager.PacketKeepalive;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -101,20 +101,6 @@
public static final int EVENT_NETWORK_SCORE_CHANGED = BASE + 4;
/**
- * Sent by the NetworkAgent to ConnectivityService to add new UID ranges
- * to be forced into this Network. For VPNs only.
- * obj = UidRange[] to forward
- */
- public static final int EVENT_UID_RANGES_ADDED = BASE + 5;
-
- /**
- * Sent by the NetworkAgent to ConnectivityService to remove UID ranges
- * from being forced into this Network. For VPNs only.
- * obj = UidRange[] to stop forwarding
- */
- public static final int EVENT_UID_RANGES_REMOVED = BASE + 6;
-
- /**
* Sent by ConnectivityService to the NetworkAgent to inform the agent of the
* networks status - whether we could use the network or could not, due to
* either a bad network configuration (no internet link) or captive portal.
@@ -390,22 +376,6 @@
}
/**
- * Called by the VPN code when it wants to add ranges of UIDs to be routed
- * through the VPN network.
- */
- public void addUidRanges(UidRange[] ranges) {
- queueOrSendMessage(EVENT_UID_RANGES_ADDED, ranges);
- }
-
- /**
- * Called by the VPN code when it wants to remove ranges of UIDs from being routed
- * through the VPN network.
- */
- public void removeUidRanges(UidRange[] ranges) {
- queueOrSendMessage(EVENT_UID_RANGES_REMOVED, ranges);
- }
-
- /**
* Called by the bearer to indicate this network was manually selected by the user.
* This should be called before the NetworkInfo is marked CONNECTED so that this
* Network can be given special treatment at that time. If {@code acceptUnvalidated} is
diff --git a/android/net/NetworkCapabilities.java b/android/net/NetworkCapabilities.java
index f468e5d..8e05cfa 100644
--- a/android/net/NetworkCapabilities.java
+++ b/android/net/NetworkCapabilities.java
@@ -20,6 +20,8 @@
import android.net.ConnectivityManager.NetworkCallback;
import android.os.Parcel;
import android.os.Parcelable;
+import android.util.ArraySet;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.BitUtils;
@@ -28,6 +30,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
+import java.util.Set;
import java.util.StringJoiner;
/**
@@ -46,6 +49,7 @@
*/
public final class NetworkCapabilities implements Parcelable {
private static final String TAG = "NetworkCapabilities";
+ private static final int INVALID_UID = -1;
/**
* @hide
@@ -63,6 +67,8 @@
mLinkDownBandwidthKbps = nc.mLinkDownBandwidthKbps;
mNetworkSpecifier = nc.mNetworkSpecifier;
mSignalStrength = nc.mSignalStrength;
+ mUids = nc.mUids;
+ mEstablishingVpnAppUid = nc.mEstablishingVpnAppUid;
}
}
@@ -76,6 +82,8 @@
mLinkUpBandwidthKbps = mLinkDownBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED;
mNetworkSpecifier = null;
mSignalStrength = SIGNAL_STRENGTH_UNSPECIFIED;
+ mUids = null;
+ mEstablishingVpnAppUid = INVALID_UID;
}
/**
@@ -107,6 +115,7 @@
NET_CAPABILITY_CAPTIVE_PORTAL,
NET_CAPABILITY_NOT_ROAMING,
NET_CAPABILITY_FOREGROUND,
+ NET_CAPABILITY_NOT_CONGESTED,
})
public @interface NetCapability { }
@@ -234,8 +243,17 @@
*/
public static final int NET_CAPABILITY_FOREGROUND = 19;
+ /**
+ * Indicates that this network is not congested.
+ * <p>
+ * When a network is congested, the device should defer network traffic that
+ * can be done at a later time without breaking developer contracts.
+ * @hide
+ */
+ public static final int NET_CAPABILITY_NOT_CONGESTED = 20;
+
private static final int MIN_NET_CAPABILITY = NET_CAPABILITY_MMS;
- private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_FOREGROUND;
+ private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_NOT_CONGESTED;
/**
* Network capabilities that are expected to be mutable, i.e., can change while a particular
@@ -248,7 +266,8 @@
(1 << NET_CAPABILITY_VALIDATED) |
(1 << NET_CAPABILITY_CAPTIVE_PORTAL) |
(1 << NET_CAPABILITY_NOT_ROAMING) |
- (1 << NET_CAPABILITY_FOREGROUND);
+ (1 << NET_CAPABILITY_FOREGROUND) |
+ (1 << NET_CAPABILITY_NOT_CONGESTED);
/**
* Network capabilities that are not allowed in NetworkRequests. This exists because the
@@ -386,12 +405,9 @@
* @hide
*/
public String describeFirstNonRequestableCapability() {
- if (hasCapability(NET_CAPABILITY_VALIDATED)) return "NET_CAPABILITY_VALIDATED";
- if (hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) return "NET_CAPABILITY_CAPTIVE_PORTAL";
- if (hasCapability(NET_CAPABILITY_FOREGROUND)) return "NET_CAPABILITY_FOREGROUND";
- // This cannot happen unless the preceding checks are incomplete.
- if ((mNetworkCapabilities & NON_REQUESTABLE_CAPABILITIES) != 0) {
- return "unknown non-requestable capabilities " + Long.toHexString(mNetworkCapabilities);
+ final long nonRequestable = (mNetworkCapabilities & NON_REQUESTABLE_CAPABILITIES);
+ if (nonRequestable != 0) {
+ return capabilityNameOf(BitUtils.unpackBits(nonRequestable)[0]);
}
if (mLinkUpBandwidthKbps != 0 || mLinkDownBandwidthKbps != 0) return "link bandwidth";
if (hasSignalStrength()) return "signalStrength";
@@ -610,6 +626,29 @@
}
/**
+ * UID of the app that manages this network, or INVALID_UID if none/unknown.
+ *
+ * This field keeps track of the UID of the app that created this network and is in charge
+ * of managing it. In the practice, it is used to store the UID of VPN apps so it is named
+ * accordingly, but it may be renamed if other mechanisms are offered for third party apps
+ * to create networks.
+ *
+ * Because this field is only used in the services side (and to avoid apps being able to
+ * set this to whatever they want), this field is not parcelled and will not be conserved
+ * across the IPC boundary.
+ * @hide
+ */
+ private int mEstablishingVpnAppUid = INVALID_UID;
+
+ /**
+ * Set the UID of the managing app.
+ * @hide
+ */
+ public void setEstablishingVpnAppUid(final int uid) {
+ mEstablishingVpnAppUid = uid;
+ }
+
+ /**
* Value indicating that link bandwidth is unspecified.
* @hide
*/
@@ -828,6 +867,174 @@
}
/**
+ * List of UIDs this network applies to. No restriction if null.
+ * <p>
+ * This is typically (and at this time, only) used by VPN. This network is only available to
+ * the UIDs in this list, and it is their default network. Apps in this list that wish to
+ * bypass the VPN can do so iff the VPN app allows them to or if they are privileged. If this
+ * member is null, then the network is not restricted by app UID. If it's an empty list, then
+ * it means nobody can use it.
+ * As a special exception, the app managing this network (as identified by its UID stored in
+ * mEstablishingVpnAppUid) can always see this network. This is embodied by a special check in
+ * satisfiedByUids. That still does not mean the network necessarily <strong>applies</strong>
+ * to the app that manages it as determined by #appliesToUid.
+ * <p>
+ * Please note that in principle a single app can be associated with multiple UIDs because
+ * each app will have a different UID when it's run as a different (macro-)user. A single
+ * macro user can only have a single active VPN app at any given time however.
+ * <p>
+ * Also please be aware this class does not try to enforce any normalization on this. Callers
+ * can only alter the UIDs by setting them wholesale : this class does not provide any utility
+ * to add or remove individual UIDs or ranges. If callers have any normalization needs on
+ * their own (like requiring sortedness or no overlap) they need to enforce it
+ * themselves. Some of the internal methods also assume this is normalized as in no adjacent
+ * or overlapping ranges are present.
+ *
+ * @hide
+ */
+ private ArraySet<UidRange> mUids = null;
+
+ /**
+ * Convenience method to set the UIDs this network applies to to a single UID.
+ * @hide
+ */
+ public NetworkCapabilities setSingleUid(int uid) {
+ final ArraySet<UidRange> identity = new ArraySet<>(1);
+ identity.add(new UidRange(uid, uid));
+ setUids(identity);
+ return this;
+ }
+
+ /**
+ * Set the list of UIDs this network applies to.
+ * This makes a copy of the set so that callers can't modify it after the call.
+ * @hide
+ */
+ public NetworkCapabilities setUids(Set<UidRange> uids) {
+ if (null == uids) {
+ mUids = null;
+ } else {
+ mUids = new ArraySet<>(uids);
+ }
+ return this;
+ }
+
+ /**
+ * Get the list of UIDs this network applies to.
+ * This returns a copy of the set so that callers can't modify the original object.
+ * @hide
+ */
+ public Set<UidRange> getUids() {
+ return null == mUids ? null : new ArraySet<>(mUids);
+ }
+
+ /**
+ * Test whether this network applies to this UID.
+ * @hide
+ */
+ public boolean appliesToUid(int uid) {
+ if (null == mUids) return true;
+ for (UidRange range : mUids) {
+ if (range.contains(uid)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Tests if the set of UIDs that this network applies to is the same of the passed set of UIDs.
+ * <p>
+ * This test only checks whether equal range objects are in both sets. It will
+ * return false if the ranges are not exactly the same, even if the covered UIDs
+ * are for an equivalent result.
+ * <p>
+ * Note that this method is not very optimized, which is fine as long as it's not used very
+ * often.
+ * <p>
+ * nc is assumed nonnull.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public boolean equalsUids(NetworkCapabilities nc) {
+ Set<UidRange> comparedUids = nc.mUids;
+ if (null == comparedUids) return null == mUids;
+ if (null == mUids) return false;
+ // Make a copy so it can be mutated to check that all ranges in mUids
+ // also are in uids.
+ final Set<UidRange> uids = new ArraySet<>(mUids);
+ for (UidRange range : comparedUids) {
+ if (!uids.contains(range)) {
+ return false;
+ }
+ uids.remove(range);
+ }
+ return uids.isEmpty();
+ }
+
+ /**
+ * Test whether the passed NetworkCapabilities satisfies the UIDs this capabilities require.
+ *
+ * This method is called on the NetworkCapabilities embedded in a request with the
+ * capabilities of an available network. It checks whether all the UIDs from this listen
+ * (representing the UIDs that must have access to the network) are satisfied by the UIDs
+ * in the passed nc (representing the UIDs that this network is available to).
+ * <p>
+ * As a special exception, the UID that created the passed network (as represented by its
+ * mEstablishingVpnAppUid field) always satisfies a NetworkRequest requiring it (of LISTEN
+ * or REQUEST types alike), even if the network does not apply to it. That is so a VPN app
+ * can see its own network when it listens for it.
+ * <p>
+ * nc is assumed nonnull. Else, NPE.
+ * @see #appliesToUid
+ * @hide
+ */
+ public boolean satisfiedByUids(NetworkCapabilities nc) {
+ if (null == nc.mUids) return true; // The network satisfies everything.
+ if (null == mUids) return false; // Not everything allowed but requires everything
+ for (UidRange requiredRange : mUids) {
+ if (requiredRange.contains(nc.mEstablishingVpnAppUid)) return true;
+ if (!nc.appliesToUidRange(requiredRange)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns whether this network applies to the passed ranges.
+ * This assumes that to apply, the passed range has to be entirely contained
+ * within one of the ranges this network applies to. If the ranges are not normalized,
+ * this method may return false even though all required UIDs are covered because no
+ * single range contained them all.
+ * @hide
+ */
+ @VisibleForTesting
+ public boolean appliesToUidRange(UidRange requiredRange) {
+ if (null == mUids) return true;
+ for (UidRange uidRange : mUids) {
+ if (uidRange.containsRange(requiredRange)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Combine the UIDs this network currently applies to with the UIDs the passed
+ * NetworkCapabilities apply to.
+ * nc is assumed nonnull.
+ */
+ private void combineUids(NetworkCapabilities nc) {
+ if (null == nc.mUids || null == mUids) {
+ mUids = null;
+ return;
+ }
+ mUids.addAll(nc.mUids);
+ }
+
+ /**
* Combine a set of Capabilities to this one. Useful for coming up with the complete set
* @hide
*/
@@ -837,6 +1044,7 @@
combineLinkBandwidths(nc);
combineSpecifiers(nc);
combineSignalStrength(nc);
+ combineUids(nc);
}
/**
@@ -849,12 +1057,13 @@
* @hide
*/
private boolean satisfiedByNetworkCapabilities(NetworkCapabilities nc, boolean onlyImmutable) {
- return (nc != null &&
- satisfiedByNetCapabilities(nc, onlyImmutable) &&
- satisfiedByTransportTypes(nc) &&
- (onlyImmutable || satisfiedByLinkBandwidths(nc)) &&
- satisfiedBySpecifier(nc) &&
- (onlyImmutable || satisfiedBySignalStrength(nc)));
+ return (nc != null
+ && satisfiedByNetCapabilities(nc, onlyImmutable)
+ && satisfiedByTransportTypes(nc)
+ && (onlyImmutable || satisfiedByLinkBandwidths(nc))
+ && satisfiedBySpecifier(nc)
+ && (onlyImmutable || satisfiedBySignalStrength(nc))
+ && (onlyImmutable || satisfiedByUids(nc)));
}
/**
@@ -935,24 +1144,26 @@
@Override
public boolean equals(Object obj) {
if (obj == null || (obj instanceof NetworkCapabilities == false)) return false;
- NetworkCapabilities that = (NetworkCapabilities)obj;
- return (equalsNetCapabilities(that) &&
- equalsTransportTypes(that) &&
- equalsLinkBandwidths(that) &&
- equalsSignalStrength(that) &&
- equalsSpecifier(that));
+ NetworkCapabilities that = (NetworkCapabilities) obj;
+ return (equalsNetCapabilities(that)
+ && equalsTransportTypes(that)
+ && equalsLinkBandwidths(that)
+ && equalsSignalStrength(that)
+ && equalsSpecifier(that)
+ && equalsUids(that));
}
@Override
public int hashCode() {
- return ((int)(mNetworkCapabilities & 0xFFFFFFFF) +
- ((int)(mNetworkCapabilities >> 32) * 3) +
- ((int)(mTransportTypes & 0xFFFFFFFF) * 5) +
- ((int)(mTransportTypes >> 32) * 7) +
- (mLinkUpBandwidthKbps * 11) +
- (mLinkDownBandwidthKbps * 13) +
- Objects.hashCode(mNetworkSpecifier) * 17 +
- (mSignalStrength * 19));
+ return ((int) (mNetworkCapabilities & 0xFFFFFFFF)
+ + ((int) (mNetworkCapabilities >> 32) * 3)
+ + ((int) (mTransportTypes & 0xFFFFFFFF) * 5)
+ + ((int) (mTransportTypes >> 32) * 7)
+ + (mLinkUpBandwidthKbps * 11)
+ + (mLinkDownBandwidthKbps * 13)
+ + Objects.hashCode(mNetworkSpecifier) * 17
+ + (mSignalStrength * 19)
+ + Objects.hashCode(mUids) * 23);
}
@Override
@@ -967,6 +1178,7 @@
dest.writeInt(mLinkDownBandwidthKbps);
dest.writeParcelable((Parcelable) mNetworkSpecifier, flags);
dest.writeInt(mSignalStrength);
+ dest.writeArraySet(mUids);
}
public static final Creator<NetworkCapabilities> CREATOR =
@@ -981,6 +1193,8 @@
netCap.mLinkDownBandwidthKbps = in.readInt();
netCap.mNetworkSpecifier = in.readParcelable(null);
netCap.mSignalStrength = in.readInt();
+ netCap.mUids = (ArraySet<UidRange>) in.readArraySet(
+ null /* ClassLoader, null for default */);
return netCap;
}
@Override
@@ -1013,7 +1227,37 @@
String signalStrength = (hasSignalStrength() ? " SignalStrength: " + mSignalStrength : "");
- return "[" + transports + capabilities + upBand + dnBand + specifier + signalStrength + "]";
+ String uids = (null != mUids ? " Uids: <" + mUids + ">" : "");
+
+ String establishingAppUid = " EstablishingAppUid: " + mEstablishingVpnAppUid;
+
+ return "[" + transports + capabilities + upBand + dnBand + specifier + signalStrength
+ + uids + establishingAppUid + "]";
+ }
+
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ for (int transport : getTransportTypes()) {
+ proto.write(NetworkCapabilitiesProto.TRANSPORTS, transport);
+ }
+
+ for (int capability : getCapabilities()) {
+ proto.write(NetworkCapabilitiesProto.CAPABILITIES, capability);
+ }
+
+ proto.write(NetworkCapabilitiesProto.LINK_UP_BANDWIDTH_KBPS, mLinkUpBandwidthKbps);
+ proto.write(NetworkCapabilitiesProto.LINK_DOWN_BANDWIDTH_KBPS, mLinkDownBandwidthKbps);
+
+ if (mNetworkSpecifier != null) {
+ proto.write(NetworkCapabilitiesProto.NETWORK_SPECIFIER, mNetworkSpecifier.toString());
+ }
+
+ proto.write(NetworkCapabilitiesProto.CAN_REPORT_SIGNAL_STRENGTH, hasSignalStrength());
+ proto.write(NetworkCapabilitiesProto.SIGNAL_STRENGTH, mSignalStrength);
+
+ proto.end(token);
}
/**
@@ -1054,6 +1298,7 @@
case NET_CAPABILITY_CAPTIVE_PORTAL: return "CAPTIVE_PORTAL";
case NET_CAPABILITY_NOT_ROAMING: return "NOT_ROAMING";
case NET_CAPABILITY_FOREGROUND: return "FOREGROUND";
+ case NET_CAPABILITY_NOT_CONGESTED: return "NOT_CONGESTED";
default: return Integer.toString(capability);
}
}
diff --git a/android/net/NetworkIdentity.java b/android/net/NetworkIdentity.java
index d3b3599..ce2de85 100644
--- a/android/net/NetworkIdentity.java
+++ b/android/net/NetworkIdentity.java
@@ -58,21 +58,24 @@
final String mNetworkId;
final boolean mRoaming;
final boolean mMetered;
+ final boolean mDefaultNetwork;
public NetworkIdentity(
int type, int subType, String subscriberId, String networkId, boolean roaming,
- boolean metered) {
+ boolean metered, boolean defaultNetwork) {
mType = type;
mSubType = COMBINE_SUBTYPE_ENABLED ? SUBTYPE_COMBINED : subType;
mSubscriberId = subscriberId;
mNetworkId = networkId;
mRoaming = roaming;
mMetered = metered;
+ mDefaultNetwork = defaultNetwork;
}
@Override
public int hashCode() {
- return Objects.hash(mType, mSubType, mSubscriberId, mNetworkId, mRoaming, mMetered);
+ return Objects.hash(mType, mSubType, mSubscriberId, mNetworkId, mRoaming, mMetered,
+ mDefaultNetwork);
}
@Override
@@ -82,7 +85,8 @@
return mType == ident.mType && mSubType == ident.mSubType && mRoaming == ident.mRoaming
&& Objects.equals(mSubscriberId, ident.mSubscriberId)
&& Objects.equals(mNetworkId, ident.mNetworkId)
- && mMetered == ident.mMetered;
+ && mMetered == ident.mMetered
+ && mDefaultNetwork == ident.mDefaultNetwork;
}
return false;
}
@@ -109,6 +113,7 @@
builder.append(", ROAMING");
}
builder.append(", metered=").append(mMetered);
+ builder.append(", defaultNetwork=").append(mDefaultNetwork);
return builder.append("}").toString();
}
@@ -125,6 +130,7 @@
proto.write(NetworkIdentityProto.NETWORK_ID, mNetworkId);
proto.write(NetworkIdentityProto.ROAMING, mRoaming);
proto.write(NetworkIdentityProto.METERED, mMetered);
+ proto.write(NetworkIdentityProto.DEFAULT_NETWORK, mDefaultNetwork);
proto.end(start);
}
@@ -153,6 +159,10 @@
return mMetered;
}
+ public boolean getDefaultNetwork() {
+ return mDefaultNetwork;
+ }
+
/**
* Scrub given IMSI on production builds.
*/
@@ -183,7 +193,8 @@
* Build a {@link NetworkIdentity} from the given {@link NetworkState},
* assuming that any mobile networks are using the current IMSI.
*/
- public static NetworkIdentity buildNetworkIdentity(Context context, NetworkState state) {
+ public static NetworkIdentity buildNetworkIdentity(Context context, NetworkState state,
+ boolean defaultNetwork) {
final int type = state.networkInfo.getType();
final int subType = state.networkInfo.getSubtype();
@@ -216,7 +227,8 @@
}
}
- return new NetworkIdentity(type, subType, subscriberId, networkId, roaming, metered);
+ return new NetworkIdentity(type, subType, subscriberId, networkId, roaming, metered,
+ defaultNetwork);
}
@Override
@@ -237,6 +249,9 @@
if (res == 0) {
res = Boolean.compare(mMetered, another.mMetered);
}
+ if (res == 0) {
+ res = Boolean.compare(mDefaultNetwork, another.mDefaultNetwork);
+ }
return res;
}
}
diff --git a/android/net/NetworkPolicyManager.java b/android/net/NetworkPolicyManager.java
index 81c49a3..2c5a021 100644
--- a/android/net/NetworkPolicyManager.java
+++ b/android/net/NetworkPolicyManager.java
@@ -29,7 +29,6 @@
import android.net.wifi.WifiInfo;
import android.os.RemoteException;
import android.os.UserHandle;
-import android.telephony.SubscriptionPlan;
import android.util.DebugUtils;
import android.util.Pair;
@@ -114,6 +113,9 @@
*/
public static final String EXTRA_NETWORK_TEMPLATE = "android.net.NETWORK_TEMPLATE";
+ public static final int OVERRIDE_UNMETERED = 1 << 0;
+ public static final int OVERRIDE_CONGESTED = 1 << 1;
+
private final Context mContext;
private INetworkPolicyManager mService;
@@ -329,7 +331,7 @@
* to access network when the device is idle or in battery saver mode. Otherwise, false.
*/
public static boolean isProcStateAllowedWhileIdleOrPowerSaveMode(int procState) {
- return procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+ return procState <= ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
}
/**
@@ -337,7 +339,7 @@
* to access network when the device is in data saver mode. Otherwise, false.
*/
public static boolean isProcStateAllowedWhileOnRestrictBackground(int procState) {
- return procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+ return procState <= ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
}
public static String resolveNetworkId(WifiConfiguration config) {
@@ -348,4 +350,13 @@
public static String resolveNetworkId(String ssid) {
return WifiInfo.removeDoubleQuotes(ssid);
}
+
+ /** {@hide} */
+ public static class Listener extends INetworkPolicyListener.Stub {
+ @Override public void onUidRulesChanged(int uid, int uidRules) { }
+ @Override public void onMeteredIfacesChanged(String[] meteredIfaces) { }
+ @Override public void onRestrictBackgroundChanged(boolean restrictBackground) { }
+ @Override public void onUidPoliciesChanged(int uid, int uidPolicies) { }
+ @Override public void onSubscriptionOverride(int subId, int overrideMask, int overrideValue) { }
+ }
}
diff --git a/android/net/NetworkRequest.java b/android/net/NetworkRequest.java
index 97ded2d..a072409 100644
--- a/android/net/NetworkRequest.java
+++ b/android/net/NetworkRequest.java
@@ -20,6 +20,7 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
+import android.util.proto.ProtoOutputStream;
import java.util.Objects;
@@ -389,6 +390,35 @@
", " + networkCapabilities.toString() + " ]";
}
+ private int typeToProtoEnum(Type t) {
+ switch (t) {
+ case NONE:
+ return NetworkRequestProto.TYPE_NONE;
+ case LISTEN:
+ return NetworkRequestProto.TYPE_LISTEN;
+ case TRACK_DEFAULT:
+ return NetworkRequestProto.TYPE_TRACK_DEFAULT;
+ case REQUEST:
+ return NetworkRequestProto.TYPE_REQUEST;
+ case BACKGROUND_REQUEST:
+ return NetworkRequestProto.TYPE_BACKGROUND_REQUEST;
+ default:
+ return NetworkRequestProto.TYPE_UNKNOWN;
+ }
+ }
+
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(NetworkRequestProto.TYPE, typeToProtoEnum(type));
+ proto.write(NetworkRequestProto.REQUEST_ID, requestId);
+ proto.write(NetworkRequestProto.LEGACY_TYPE, legacyType);
+ networkCapabilities.writeToProto(proto, NetworkRequestProto.NETWORK_CAPABILITIES);
+
+ proto.end(token);
+ }
+
public boolean equals(Object obj) {
if (obj instanceof NetworkRequest == false) return false;
NetworkRequest that = (NetworkRequest)obj;
diff --git a/android/net/NetworkStats.java b/android/net/NetworkStats.java
index 171adc0..01b2b39 100644
--- a/android/net/NetworkStats.java
+++ b/android/net/NetworkStats.java
@@ -82,6 +82,13 @@
/** {@link #roaming} value where roaming data is accounted. */
public static final int ROAMING_YES = 1;
+ /** {@link #onDefaultNetwork} value to account for all default network states. */
+ public static final int DEFAULT_NETWORK_ALL = -1;
+ /** {@link #onDefaultNetwork} value to account for usage while not the default network. */
+ public static final int DEFAULT_NETWORK_NO = 0;
+ /** {@link #onDefaultNetwork} value to account for usage while the default network. */
+ public static final int DEFAULT_NETWORK_YES = 1;
+
/** Denotes a request for stats at the interface level. */
public static final int STATS_PER_IFACE = 0;
/** Denotes a request for stats at the interface and UID level. */
@@ -102,6 +109,7 @@
private int[] tag;
private int[] metered;
private int[] roaming;
+ private int[] defaultNetwork;
private long[] rxBytes;
private long[] rxPackets;
private long[] txBytes;
@@ -125,6 +133,12 @@
* getSummary().
*/
public int roaming;
+ /**
+ * Note that this is only populated w/ the default value when read from /proc or written
+ * to disk. We merge in the correct value when reporting this value to clients of
+ * getSummary().
+ */
+ public int defaultNetwork;
public long rxBytes;
public long rxPackets;
public long txBytes;
@@ -142,18 +156,20 @@
public Entry(String iface, int uid, int set, int tag, long rxBytes, long rxPackets,
long txBytes, long txPackets, long operations) {
- this(iface, uid, set, tag, METERED_NO, ROAMING_NO, rxBytes, rxPackets, txBytes,
- txPackets, operations);
+ this(iface, uid, set, tag, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO,
+ rxBytes, rxPackets, txBytes, txPackets, operations);
}
public Entry(String iface, int uid, int set, int tag, int metered, int roaming,
- long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) {
+ int defaultNetwork, long rxBytes, long rxPackets, long txBytes, long txPackets,
+ long operations) {
this.iface = iface;
this.uid = uid;
this.set = set;
this.tag = tag;
this.metered = metered;
this.roaming = roaming;
+ this.defaultNetwork = defaultNetwork;
this.rxBytes = rxBytes;
this.rxPackets = rxPackets;
this.txBytes = txBytes;
@@ -187,6 +203,7 @@
builder.append(" tag=").append(tagToString(tag));
builder.append(" metered=").append(meteredToString(metered));
builder.append(" roaming=").append(roamingToString(roaming));
+ builder.append(" defaultNetwork=").append(defaultNetworkToString(defaultNetwork));
builder.append(" rxBytes=").append(rxBytes);
builder.append(" rxPackets=").append(rxPackets);
builder.append(" txBytes=").append(txBytes);
@@ -200,7 +217,8 @@
if (o instanceof Entry) {
final Entry e = (Entry) o;
return uid == e.uid && set == e.set && tag == e.tag && metered == e.metered
- && roaming == e.roaming && rxBytes == e.rxBytes && rxPackets == e.rxPackets
+ && roaming == e.roaming && defaultNetwork == e.defaultNetwork
+ && rxBytes == e.rxBytes && rxPackets == e.rxPackets
&& txBytes == e.txBytes && txPackets == e.txPackets
&& operations == e.operations && iface.equals(e.iface);
}
@@ -209,7 +227,7 @@
@Override
public int hashCode() {
- return Objects.hash(uid, set, tag, metered, roaming, iface);
+ return Objects.hash(uid, set, tag, metered, roaming, defaultNetwork, iface);
}
}
@@ -224,6 +242,7 @@
this.tag = new int[initialSize];
this.metered = new int[initialSize];
this.roaming = new int[initialSize];
+ this.defaultNetwork = new int[initialSize];
this.rxBytes = new long[initialSize];
this.rxPackets = new long[initialSize];
this.txBytes = new long[initialSize];
@@ -238,6 +257,7 @@
this.tag = EmptyArray.INT;
this.metered = EmptyArray.INT;
this.roaming = EmptyArray.INT;
+ this.defaultNetwork = EmptyArray.INT;
this.rxBytes = EmptyArray.LONG;
this.rxPackets = EmptyArray.LONG;
this.txBytes = EmptyArray.LONG;
@@ -256,6 +276,7 @@
tag = parcel.createIntArray();
metered = parcel.createIntArray();
roaming = parcel.createIntArray();
+ defaultNetwork = parcel.createIntArray();
rxBytes = parcel.createLongArray();
rxPackets = parcel.createLongArray();
txBytes = parcel.createLongArray();
@@ -274,6 +295,7 @@
dest.writeIntArray(tag);
dest.writeIntArray(metered);
dest.writeIntArray(roaming);
+ dest.writeIntArray(defaultNetwork);
dest.writeLongArray(rxBytes);
dest.writeLongArray(rxPackets);
dest.writeLongArray(txBytes);
@@ -308,10 +330,11 @@
@VisibleForTesting
public NetworkStats addValues(String iface, int uid, int set, int tag, int metered, int roaming,
- long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) {
+ int defaultNetwork, long rxBytes, long rxPackets, long txBytes, long txPackets,
+ long operations) {
return addValues(new Entry(
- iface, uid, set, tag, metered, roaming, rxBytes, rxPackets, txBytes, txPackets,
- operations));
+ iface, uid, set, tag, metered, roaming, defaultNetwork, rxBytes, rxPackets,
+ txBytes, txPackets, operations));
}
/**
@@ -327,6 +350,7 @@
tag = Arrays.copyOf(tag, newLength);
metered = Arrays.copyOf(metered, newLength);
roaming = Arrays.copyOf(roaming, newLength);
+ defaultNetwork = Arrays.copyOf(defaultNetwork, newLength);
rxBytes = Arrays.copyOf(rxBytes, newLength);
rxPackets = Arrays.copyOf(rxPackets, newLength);
txBytes = Arrays.copyOf(txBytes, newLength);
@@ -341,6 +365,7 @@
tag[size] = entry.tag;
metered[size] = entry.metered;
roaming[size] = entry.roaming;
+ defaultNetwork[size] = entry.defaultNetwork;
rxBytes[size] = entry.rxBytes;
rxPackets[size] = entry.rxPackets;
txBytes[size] = entry.txBytes;
@@ -362,6 +387,7 @@
entry.tag = tag[i];
entry.metered = metered[i];
entry.roaming = roaming[i];
+ entry.defaultNetwork = defaultNetwork[i];
entry.rxBytes = rxBytes[i];
entry.rxPackets = rxPackets[i];
entry.txBytes = txBytes[i];
@@ -416,7 +442,7 @@
*/
public NetworkStats combineValues(Entry entry) {
final int i = findIndex(entry.iface, entry.uid, entry.set, entry.tag, entry.metered,
- entry.roaming);
+ entry.roaming, entry.defaultNetwork);
if (i == -1) {
// only create new entry when positive contribution
addValues(entry);
@@ -444,10 +470,12 @@
/**
* Find first stats index that matches the requested parameters.
*/
- public int findIndex(String iface, int uid, int set, int tag, int metered, int roaming) {
+ public int findIndex(String iface, int uid, int set, int tag, int metered, int roaming,
+ int defaultNetwork) {
for (int i = 0; i < size; i++) {
if (uid == this.uid[i] && set == this.set[i] && tag == this.tag[i]
&& metered == this.metered[i] && roaming == this.roaming[i]
+ && defaultNetwork == this.defaultNetwork[i]
&& Objects.equals(iface, this.iface[i])) {
return i;
}
@@ -461,7 +489,7 @@
*/
@VisibleForTesting
public int findIndexHinted(String iface, int uid, int set, int tag, int metered, int roaming,
- int hintIndex) {
+ int defaultNetwork, int hintIndex) {
for (int offset = 0; offset < size; offset++) {
final int halfOffset = offset / 2;
@@ -475,6 +503,7 @@
if (uid == this.uid[i] && set == this.set[i] && tag == this.tag[i]
&& metered == this.metered[i] && roaming == this.roaming[i]
+ && defaultNetwork == this.defaultNetwork[i]
&& Objects.equals(iface, this.iface[i])) {
return i;
}
@@ -489,7 +518,8 @@
*/
public void spliceOperationsFrom(NetworkStats stats) {
for (int i = 0; i < size; i++) {
- final int j = stats.findIndex(iface[i], uid[i], set[i], tag[i], metered[i], roaming[i]);
+ final int j = stats.findIndex(iface[i], uid[i], set[i], tag[i], metered[i], roaming[i],
+ defaultNetwork[i]);
if (j == -1) {
operations[i] = 0;
} else {
@@ -581,6 +611,7 @@
entry.tag = TAG_NONE;
entry.metered = METERED_ALL;
entry.roaming = ROAMING_ALL;
+ entry.defaultNetwork = DEFAULT_NETWORK_ALL;
entry.rxBytes = 0;
entry.rxPackets = 0;
entry.txBytes = 0;
@@ -677,6 +708,7 @@
entry.tag = left.tag[i];
entry.metered = left.metered[i];
entry.roaming = left.roaming[i];
+ entry.defaultNetwork = left.defaultNetwork[i];
entry.rxBytes = left.rxBytes[i];
entry.rxPackets = left.rxPackets[i];
entry.txBytes = left.txBytes[i];
@@ -685,7 +717,7 @@
// find remote row that matches, and subtract
final int j = right.findIndexHinted(entry.iface, entry.uid, entry.set, entry.tag,
- entry.metered, entry.roaming, i);
+ entry.metered, entry.roaming, entry.defaultNetwork, i);
if (j != -1) {
// Found matching row, subtract remote value.
entry.rxBytes -= right.rxBytes[j];
@@ -725,6 +757,7 @@
entry.tag = TAG_NONE;
entry.metered = METERED_ALL;
entry.roaming = ROAMING_ALL;
+ entry.defaultNetwork = DEFAULT_NETWORK_ALL;
entry.operations = 0L;
for (int i = 0; i < size; i++) {
@@ -755,6 +788,7 @@
entry.tag = TAG_NONE;
entry.metered = METERED_ALL;
entry.roaming = ROAMING_ALL;
+ entry.defaultNetwork = DEFAULT_NETWORK_ALL;
for (int i = 0; i < size; i++) {
// skip specific tags, since already counted in TAG_NONE
@@ -802,6 +836,7 @@
pw.print(" tag="); pw.print(tagToString(tag[i]));
pw.print(" metered="); pw.print(meteredToString(metered[i]));
pw.print(" roaming="); pw.print(roamingToString(roaming[i]));
+ pw.print(" defaultNetwork="); pw.print(defaultNetworkToString(defaultNetwork[i]));
pw.print(" rxBytes="); pw.print(rxBytes[i]);
pw.print(" rxPackets="); pw.print(rxPackets[i]);
pw.print(" txBytes="); pw.print(txBytes[i]);
@@ -900,6 +935,22 @@
}
}
+ /**
+ * Return text description of {@link #defaultNetwork} value.
+ */
+ public static String defaultNetworkToString(int defaultNetwork) {
+ switch (defaultNetwork) {
+ case DEFAULT_NETWORK_ALL:
+ return "ALL";
+ case DEFAULT_NETWORK_NO:
+ return "NO";
+ case DEFAULT_NETWORK_YES:
+ return "YES";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
@Override
public String toString() {
final CharArrayWriter writer = new CharArrayWriter();
@@ -1055,6 +1106,7 @@
tmpEntry.set = set[i];
tmpEntry.metered = metered[i];
tmpEntry.roaming = roaming[i];
+ tmpEntry.defaultNetwork = defaultNetwork[i];
combineValues(tmpEntry);
if (tag[i] == TAG_NONE) {
moved.add(tmpEntry);
@@ -1075,6 +1127,7 @@
moved.iface = underlyingIface;
moved.metered = METERED_ALL;
moved.roaming = ROAMING_ALL;
+ moved.defaultNetwork = DEFAULT_NETWORK_ALL;
combineValues(moved);
// Caveat: if the vpn software uses tag, the total tagged traffic may be greater than
@@ -1085,13 +1138,13 @@
// roaming data after applying these adjustments, by checking the NetworkIdentity of the
// underlying iface.
int idxVpnBackground = findIndex(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO);
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO);
if (idxVpnBackground != -1) {
tunSubtract(idxVpnBackground, this, moved);
}
int idxVpnForeground = findIndex(underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE,
- METERED_NO, ROAMING_NO);
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO);
if (idxVpnForeground != -1) {
tunSubtract(idxVpnForeground, this, moved);
}
diff --git a/android/net/NetworkTemplate.java b/android/net/NetworkTemplate.java
index b307c5d..8efd39a 100644
--- a/android/net/NetworkTemplate.java
+++ b/android/net/NetworkTemplate.java
@@ -24,6 +24,15 @@
import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
import static android.net.ConnectivityManager.TYPE_WIMAX;
import static android.net.NetworkIdentity.COMBINE_SUBTYPE_ENABLED;
+import static android.net.NetworkStats.DEFAULT_NETWORK_ALL;
+import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
+import static android.net.NetworkStats.METERED_ALL;
+import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkStats.ROAMING_ALL;
+import static android.net.NetworkStats.ROAMING_NO;
+import static android.net.NetworkStats.ROAMING_YES;
import static android.net.wifi.WifiInfo.removeDoubleQuotes;
import static android.telephony.TelephonyManager.NETWORK_CLASS_2_G;
import static android.telephony.TelephonyManager.NETWORK_CLASS_3_G;
@@ -191,16 +200,30 @@
private final String mNetworkId;
+ // Matches for the NetworkStats constants METERED_*, ROAMING_* and DEFAULT_NETWORK_*.
+ private final int mMetered;
+ private final int mRoaming;
+ private final int mDefaultNetwork;
+
public NetworkTemplate(int matchRule, String subscriberId, String networkId) {
this(matchRule, subscriberId, new String[] { subscriberId }, networkId);
}
public NetworkTemplate(int matchRule, String subscriberId, String[] matchSubscriberIds,
String networkId) {
+ this(matchRule, subscriberId, matchSubscriberIds, networkId, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL);
+ }
+
+ public NetworkTemplate(int matchRule, String subscriberId, String[] matchSubscriberIds,
+ String networkId, int metered, int roaming, int defaultNetwork) {
mMatchRule = matchRule;
mSubscriberId = subscriberId;
mMatchSubscriberIds = matchSubscriberIds;
mNetworkId = networkId;
+ mMetered = metered;
+ mRoaming = roaming;
+ mDefaultNetwork = defaultNetwork;
if (!isKnownMatchRule(matchRule)) {
Log.e(TAG, "Unknown network template rule " + matchRule
@@ -213,6 +236,9 @@
mSubscriberId = in.readString();
mMatchSubscriberIds = in.createStringArray();
mNetworkId = in.readString();
+ mMetered = in.readInt();
+ mRoaming = in.readInt();
+ mDefaultNetwork = in.readInt();
}
@Override
@@ -221,6 +247,9 @@
dest.writeString(mSubscriberId);
dest.writeStringArray(mMatchSubscriberIds);
dest.writeString(mNetworkId);
+ dest.writeInt(mMetered);
+ dest.writeInt(mRoaming);
+ dest.writeInt(mDefaultNetwork);
}
@Override
@@ -243,12 +272,23 @@
if (mNetworkId != null) {
builder.append(", networkId=").append(mNetworkId);
}
+ if (mMetered != METERED_ALL) {
+ builder.append(", metered=").append(NetworkStats.meteredToString(mMetered));
+ }
+ if (mRoaming != ROAMING_ALL) {
+ builder.append(", roaming=").append(NetworkStats.roamingToString(mRoaming));
+ }
+ if (mDefaultNetwork != DEFAULT_NETWORK_ALL) {
+ builder.append(", defaultNetwork=").append(NetworkStats.defaultNetworkToString(
+ mDefaultNetwork));
+ }
return builder.toString();
}
@Override
public int hashCode() {
- return Objects.hash(mMatchRule, mSubscriberId, mNetworkId);
+ return Objects.hash(mMatchRule, mSubscriberId, mNetworkId, mMetered, mRoaming,
+ mDefaultNetwork);
}
@Override
@@ -257,7 +297,10 @@
final NetworkTemplate other = (NetworkTemplate) obj;
return mMatchRule == other.mMatchRule
&& Objects.equals(mSubscriberId, other.mSubscriberId)
- && Objects.equals(mNetworkId, other.mNetworkId);
+ && Objects.equals(mNetworkId, other.mNetworkId)
+ && mMetered == other.mMetered
+ && mRoaming == other.mRoaming
+ && mDefaultNetwork == other.mDefaultNetwork;
}
return false;
}
@@ -300,6 +343,10 @@
* Test if given {@link NetworkIdentity} matches this template.
*/
public boolean matches(NetworkIdentity ident) {
+ if (!matchesMetered(ident)) return false;
+ if (!matchesRoaming(ident)) return false;
+ if (!matchesDefaultNetwork(ident)) return false;
+
switch (mMatchRule) {
case MATCH_MOBILE_ALL:
return matchesMobile(ident);
@@ -326,6 +373,24 @@
}
}
+ private boolean matchesMetered(NetworkIdentity ident) {
+ return (mMetered == METERED_ALL)
+ || (mMetered == METERED_YES && ident.mMetered)
+ || (mMetered == METERED_NO && !ident.mMetered);
+ }
+
+ private boolean matchesRoaming(NetworkIdentity ident) {
+ return (mRoaming == ROAMING_ALL)
+ || (mRoaming == ROAMING_YES && ident.mRoaming)
+ || (mRoaming == ROAMING_NO && !ident.mRoaming);
+ }
+
+ private boolean matchesDefaultNetwork(NetworkIdentity ident) {
+ return (mDefaultNetwork == DEFAULT_NETWORK_ALL)
+ || (mDefaultNetwork == DEFAULT_NETWORK_YES && ident.mDefaultNetwork)
+ || (mDefaultNetwork == DEFAULT_NETWORK_NO && !ident.mDefaultNetwork);
+ }
+
public boolean matchesSubscriberId(String subscriberId) {
return ArrayUtils.contains(mMatchSubscriberIds, subscriberId);
}
diff --git a/android/net/NetworkWatchlistManager.java b/android/net/NetworkWatchlistManager.java
index 42e43c8..49047d3 100644
--- a/android/net/NetworkWatchlistManager.java
+++ b/android/net/NetworkWatchlistManager.java
@@ -59,8 +59,8 @@
/**
* Report network watchlist records if necessary.
*
- * Watchlist report process will run summarize records into a single report, then the
- * report will be processed by differential privacy framework and store it on disk.
+ * Watchlist report process will summarize records into a single report, then the
+ * report will be processed by differential privacy framework and stored on disk.
*
* @hide
*/
@@ -72,4 +72,30 @@
e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Reload network watchlist.
+ *
+ * @hide
+ */
+ public void reloadWatchlist() {
+ try {
+ mNetworkWatchlistManager.reloadWatchlist();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to reload watchlist");
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get Network Watchlist config file hash.
+ */
+ public byte[] getWatchlistConfigHash() {
+ try {
+ return mNetworkWatchlistManager.getWatchlistConfigHash();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to get watchlist config hash");
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/android/net/TrafficStats.java b/android/net/TrafficStats.java
index 196a3bc..bda720b 100644
--- a/android/net/TrafficStats.java
+++ b/android/net/TrafficStats.java
@@ -27,6 +27,7 @@
import android.media.MediaPlayer;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.util.DataUnit;
import com.android.server.NetworkManagementSocketTagger;
@@ -56,15 +57,20 @@
*/
public final static int UNSUPPORTED = -1;
- /** @hide */
+ /** @hide @deprecated use {@link DataUnit} instead to clarify SI-vs-IEC */
+ @Deprecated
public static final long KB_IN_BYTES = 1024;
- /** @hide */
+ /** @hide @deprecated use {@link DataUnit} instead to clarify SI-vs-IEC */
+ @Deprecated
public static final long MB_IN_BYTES = KB_IN_BYTES * 1024;
- /** @hide */
+ /** @hide @deprecated use {@link DataUnit} instead to clarify SI-vs-IEC */
+ @Deprecated
public static final long GB_IN_BYTES = MB_IN_BYTES * 1024;
- /** @hide */
+ /** @hide @deprecated use {@link DataUnit} instead to clarify SI-vs-IEC */
+ @Deprecated
public static final long TB_IN_BYTES = GB_IN_BYTES * 1024;
- /** @hide */
+ /** @hide @deprecated use {@link DataUnit} instead to clarify SI-vs-IEC */
+ @Deprecated
public static final long PB_IN_BYTES = TB_IN_BYTES * 1024;
/**
diff --git a/android/net/apf/ApfFilter.java b/android/net/apf/ApfFilter.java
index 31a1abb..7d9736e 100644
--- a/android/net/apf/ApfFilter.java
+++ b/android/net/apf/ApfFilter.java
@@ -38,6 +38,7 @@
import android.net.metrics.ApfStats;
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.RaEvent;
+import android.net.util.InterfaceParams;
import android.system.ErrnoException;
import android.system.Os;
import android.system.PacketSocketAddress;
@@ -56,7 +57,6 @@
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
-import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
@@ -247,7 +247,7 @@
private final ApfCapabilities mApfCapabilities;
private final IpClient.Callback mIpClientCallback;
- private final NetworkInterface mNetworkInterface;
+ private final InterfaceParams mInterfaceParams;
private final IpConnectivityLog mMetricsLog;
@VisibleForTesting
@@ -269,11 +269,11 @@
private int mIPv4PrefixLength;
@VisibleForTesting
- ApfFilter(ApfConfiguration config, NetworkInterface networkInterface,
+ ApfFilter(ApfConfiguration config, InterfaceParams ifParams,
IpClient.Callback ipClientCallback, IpConnectivityLog log) {
mApfCapabilities = config.apfCapabilities;
mIpClientCallback = ipClientCallback;
- mNetworkInterface = networkInterface;
+ mInterfaceParams = ifParams;
mMulticastFilter = config.multicastFilter;
mDrop802_3Frames = config.ieee802_3Filter;
@@ -287,7 +287,7 @@
}
private void log(String s) {
- Log.d(TAG, "(" + mNetworkInterface.getName() + "): " + s);
+ Log.d(TAG, "(" + mInterfaceParams.name + "): " + s);
}
@GuardedBy("this")
@@ -332,14 +332,14 @@
void maybeStartFilter() {
FileDescriptor socket;
try {
- mHardwareAddress = mNetworkInterface.getHardwareAddress();
+ mHardwareAddress = mInterfaceParams.macAddr.toByteArray();
synchronized(this) {
// Install basic filters
installNewProgramLocked();
}
socket = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6);
- PacketSocketAddress addr = new PacketSocketAddress((short) ETH_P_IPV6,
- mNetworkInterface.getIndex());
+ PacketSocketAddress addr = new PacketSocketAddress(
+ (short) ETH_P_IPV6, mInterfaceParams.index);
Os.bind(socket, addr);
NetworkUtils.attachRaFilter(socket, mApfCapabilities.apfPacketFormat);
} catch(SocketException|ErrnoException e) {
@@ -1168,10 +1168,10 @@
* filtering using APF programs.
*/
public static ApfFilter maybeCreate(ApfConfiguration config,
- NetworkInterface networkInterface, IpClient.Callback ipClientCallback) {
- if (config == null) return null;
+ InterfaceParams ifParams, IpClient.Callback ipClientCallback) {
+ if (config == null || ifParams == null) return null;
ApfCapabilities apfCapabilities = config.apfCapabilities;
- if (apfCapabilities == null || networkInterface == null) return null;
+ if (apfCapabilities == null) return null;
if (apfCapabilities.apfVersionSupported == 0) return null;
if (apfCapabilities.maximumApfProgramSize < 512) {
Log.e(TAG, "Unacceptably small APF limit: " + apfCapabilities.maximumApfProgramSize);
@@ -1186,7 +1186,7 @@
Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported);
return null;
}
- return new ApfFilter(config, networkInterface, ipClientCallback, new IpConnectivityLog());
+ return new ApfFilter(config, ifParams, ipClientCallback, new IpConnectivityLog());
}
public synchronized void shutdown() {
diff --git a/android/net/dhcp/DhcpClient.java b/android/net/dhcp/DhcpClient.java
index ed78175..a956cef 100644
--- a/android/net/dhcp/DhcpClient.java
+++ b/android/net/dhcp/DhcpClient.java
@@ -34,6 +34,7 @@
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.DhcpClientEvent;
import android.net.metrics.DhcpErrorEvent;
+import android.net.util.InterfaceParams;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -50,7 +51,6 @@
import java.io.IOException;
import java.lang.Thread;
import java.net.Inet4Address;
-import java.net.NetworkInterface;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.Arrays;
@@ -187,7 +187,8 @@
private final String mIfaceName;
private boolean mRegisteredForPreDhcpNotification;
- private NetworkInterface mIface;
+ private InterfaceParams mIface;
+ // TODO: MacAddress-ify more of this class hierarchy.
private byte[] mHwAddr;
private PacketSocketAddress mInterfaceBroadcastAddr;
private int mTransactionId;
@@ -221,8 +222,9 @@
return new WakeupMessage(mContext, getHandler(), cmdName, cmd);
}
+ // TODO: Take an InterfaceParams instance instead of an interface name String.
private DhcpClient(Context context, StateMachine controller, String iface) {
- super(TAG);
+ super(TAG, controller.getHandler());
mContext = context;
mController = controller;
@@ -262,23 +264,23 @@
}
public static DhcpClient makeDhcpClient(
- Context context, StateMachine controller, String intf) {
- DhcpClient client = new DhcpClient(context, controller, intf);
+ Context context, StateMachine controller, InterfaceParams ifParams) {
+ DhcpClient client = new DhcpClient(context, controller, ifParams.name);
+ client.mIface = ifParams;
client.start();
return client;
}
private boolean initInterface() {
- try {
- mIface = NetworkInterface.getByName(mIfaceName);
- mHwAddr = mIface.getHardwareAddress();
- mInterfaceBroadcastAddr = new PacketSocketAddress(mIface.getIndex(),
- DhcpPacket.ETHER_BROADCAST);
- return true;
- } catch(SocketException | NullPointerException e) {
- Log.e(TAG, "Can't determine ifindex or MAC address for " + mIfaceName, e);
+ if (mIface == null) mIface = InterfaceParams.getByName(mIfaceName);
+ if (mIface == null) {
+ Log.e(TAG, "Can't determine InterfaceParams for " + mIfaceName);
return false;
}
+
+ mHwAddr = mIface.macAddr.toByteArray();
+ mInterfaceBroadcastAddr = new PacketSocketAddress(mIface.index, DhcpPacket.ETHER_BROADCAST);
+ return true;
}
private void startNewTransaction() {
@@ -293,7 +295,7 @@
private boolean initPacketSocket() {
try {
mPacketSock = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IP);
- PacketSocketAddress addr = new PacketSocketAddress((short) ETH_P_IP, mIface.getIndex());
+ PacketSocketAddress addr = new PacketSocketAddress((short) ETH_P_IP, mIface.index);
Os.bind(mPacketSock, addr);
NetworkUtils.attachDhcpFilter(mPacketSock);
} catch(SocketException|ErrnoException e) {
diff --git a/android/net/ip/ConnectivityPacketTracker.java b/android/net/ip/ConnectivityPacketTracker.java
index 6cf4fa9..e6ddbbc 100644
--- a/android/net/ip/ConnectivityPacketTracker.java
+++ b/android/net/ip/ConnectivityPacketTracker.java
@@ -21,6 +21,7 @@
import android.net.NetworkUtils;
import android.net.util.PacketReader;
import android.net.util.ConnectivityPacketSummary;
+import android.net.util.InterfaceParams;
import android.os.Handler;
import android.system.ErrnoException;
import android.system.Os;
@@ -35,7 +36,6 @@
import java.io.FileDescriptor;
import java.io.InterruptedIOException;
import java.io.IOException;
-import java.net.NetworkInterface;
import java.net.SocketException;
@@ -69,24 +69,12 @@
private boolean mRunning;
private String mDisplayName;
- public ConnectivityPacketTracker(Handler h, NetworkInterface netif, LocalLog log) {
- final String ifname;
- final int ifindex;
- final byte[] hwaddr;
- final int mtu;
+ public ConnectivityPacketTracker(Handler h, InterfaceParams ifParams, LocalLog log) {
+ if (ifParams == null) throw new IllegalArgumentException("null InterfaceParams");
- try {
- ifname = netif.getName();
- ifindex = netif.getIndex();
- hwaddr = netif.getHardwareAddress();
- mtu = netif.getMTU();
- } catch (NullPointerException|SocketException e) {
- throw new IllegalArgumentException("bad network interface", e);
- }
-
- mTag = TAG + "." + ifname;
+ mTag = TAG + "." + ifParams.name;
mLog = log;
- mPacketListener = new PacketListener(h, ifindex, hwaddr, mtu);
+ mPacketListener = new PacketListener(h, ifParams);
}
public void start(String displayName) {
@@ -102,13 +90,11 @@
}
private final class PacketListener extends PacketReader {
- private final int mIfIndex;
- private final byte mHwAddr[];
+ private final InterfaceParams mInterface;
- PacketListener(Handler h, int ifindex, byte[] hwaddr, int mtu) {
- super(h, mtu);
- mIfIndex = ifindex;
- mHwAddr = hwaddr;
+ PacketListener(Handler h, InterfaceParams ifParams) {
+ super(h, ifParams.defaultMtu);
+ mInterface = ifParams;
}
@Override
@@ -117,7 +103,7 @@
try {
s = Os.socket(AF_PACKET, SOCK_RAW, 0);
NetworkUtils.attachControlPacketFilter(s, ARPHRD_ETHER);
- Os.bind(s, new PacketSocketAddress((short) ETH_P_ALL, mIfIndex));
+ Os.bind(s, new PacketSocketAddress((short) ETH_P_ALL, mInterface.index));
} catch (ErrnoException | IOException e) {
logError("Failed to create packet tracking socket: ", e);
closeFd(s);
@@ -129,7 +115,7 @@
@Override
protected void handlePacket(byte[] recvbuf, int length) {
final String summary = ConnectivityPacketSummary.summarize(
- mHwAddr, recvbuf, length);
+ mInterface.macAddr, recvbuf, length);
if (summary == null) return;
if (DBG) Log.d(mTag, summary);
diff --git a/android/net/ip/IpClient.java b/android/net/ip/IpClient.java
index fdb366c..d3a97b3 100644
--- a/android/net/ip/IpClient.java
+++ b/android/net/ip/IpClient.java
@@ -35,6 +35,7 @@
import android.net.dhcp.DhcpClient;
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.IpManagerEvent;
+import android.net.util.InterfaceParams;
import android.net.util.MultinetworkPolicyTracker;
import android.net.util.NetdService;
import android.net.util.NetworkConstants;
@@ -63,7 +64,6 @@
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
-import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Collection;
@@ -556,7 +556,7 @@
private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
private final InterfaceController mInterfaceCtrl;
- private NetworkInterface mNetworkInterface;
+ private InterfaceParams mInterfaceParams;
/**
* Non-final member variables accessed only from within our StateMachine.
@@ -722,7 +722,12 @@
return;
}
- getNetworkInterface();
+ mInterfaceParams = InterfaceParams.getByName(mInterfaceName);
+ if (mInterfaceParams == null) {
+ logError("Failed to find InterfaceParams for " + mInterfaceName);
+ // TODO: call doImmediateProvisioningFailure() with an error code
+ // indicating something like "interface not ready".
+ }
mCallback.setNeighborDiscoveryOffload(true);
sendMessage(CMD_START, new ProvisioningConfiguration(req));
@@ -858,7 +863,7 @@
protected String getLogRecString(Message msg) {
final String logLine = String.format(
"%s/%d %d %d %s [%s]",
- mInterfaceName, mNetworkInterface == null ? -1 : mNetworkInterface.getIndex(),
+ mInterfaceName, (mInterfaceParams == null) ? -1 : mInterfaceParams.index,
msg.arg1, msg.arg2, Objects.toString(msg.obj), mMsgStateLogger);
final String richerLogLine = getWhatToString(msg.what) + " " + logLine;
@@ -889,15 +894,6 @@
mLog.log(msg);
}
- private void getNetworkInterface() {
- try {
- mNetworkInterface = NetworkInterface.getByName(mInterfaceName);
- } catch (SocketException | NullPointerException e) {
- // TODO: throw new IllegalStateException.
- logError("Failed to get interface object: %s", e);
- }
- }
-
// This needs to be called with care to ensure that our LinkProperties
// are in sync with the actual LinkProperties of the interface. For example,
// we should only call this if we know for sure that there are no IP addresses
@@ -1218,7 +1214,7 @@
}
} else {
// Start DHCPv4.
- mDhcpClient = DhcpClient.makeDhcpClient(mContext, IpClient.this, mInterfaceName);
+ mDhcpClient = DhcpClient.makeDhcpClient(mContext, IpClient.this, mInterfaceParams);
mDhcpClient.registerForPreDhcpNotification();
mDhcpClient.sendMessage(DhcpClient.CMD_START_DHCP);
}
@@ -1245,7 +1241,7 @@
try {
mIpReachabilityMonitor = new IpReachabilityMonitor(
mContext,
- mInterfaceName,
+ mInterfaceParams,
getHandler(),
mLog,
new IpReachabilityMonitor.Callback() {
@@ -1447,7 +1443,7 @@
mContext.getResources().getBoolean(R.bool.config_apfDrop802_3Frames);
apfConfig.ethTypeBlackList =
mContext.getResources().getIntArray(R.array.config_apfEthTypeBlackList);
- mApfFilter = ApfFilter.maybeCreate(apfConfig, mNetworkInterface, mCallback);
+ mApfFilter = ApfFilter.maybeCreate(apfConfig, mInterfaceParams, mCallback);
// TODO: investigate the effects of any multicast filtering racing/interfering with the
// rest of this IP configuration startup.
if (mApfFilter == null) {
@@ -1515,7 +1511,7 @@
private ConnectivityPacketTracker createPacketTracker() {
try {
return new ConnectivityPacketTracker(
- getHandler(), mNetworkInterface, mConnectivityPacketLog);
+ getHandler(), mInterfaceParams, mConnectivityPacketLog);
} catch (IllegalArgumentException e) {
return null;
}
diff --git a/android/net/ip/IpNeighborMonitor.java b/android/net/ip/IpNeighborMonitor.java
index 6807334..fc07aa1 100644
--- a/android/net/ip/IpNeighborMonitor.java
+++ b/android/net/ip/IpNeighborMonitor.java
@@ -16,7 +16,11 @@
package android.net.ip;
-import android.net.netlink.NetlinkConstants;
+import static android.net.netlink.NetlinkConstants.hexify;
+import static android.net.netlink.NetlinkConstants.RTM_DELNEIGH;
+import static android.net.netlink.NetlinkConstants.stringForNlMsgType;
+
+import android.net.MacAddress;
import android.net.netlink.NetlinkErrorMessage;
import android.net.netlink.NetlinkMessage;
import android.net.netlink.NetlinkSocket;
@@ -92,37 +96,35 @@
final int ifindex;
final InetAddress ip;
final short nudState;
- final byte[] linkLayerAddr;
+ final MacAddress macAddr;
public NeighborEvent(long elapsedMs, short msgType, int ifindex, InetAddress ip,
- short nudState, byte[] linkLayerAddr) {
+ short nudState, MacAddress macAddr) {
this.elapsedMs = elapsedMs;
this.msgType = msgType;
this.ifindex = ifindex;
this.ip = ip;
this.nudState = nudState;
- this.linkLayerAddr = linkLayerAddr;
+ this.macAddr = macAddr;
}
boolean isConnected() {
- return (msgType != NetlinkConstants.RTM_DELNEIGH) &&
- StructNdMsg.isNudStateConnected(nudState);
+ return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateConnected(nudState);
}
boolean isValid() {
- return (msgType != NetlinkConstants.RTM_DELNEIGH) &&
- StructNdMsg.isNudStateValid(nudState);
+ return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateValid(nudState);
}
@Override
public String toString() {
final StringJoiner j = new StringJoiner(",", "NeighborEvent{", "}");
return j.add("@" + elapsedMs)
- .add(NetlinkConstants.stringForNlMsgType(msgType))
+ .add(stringForNlMsgType(msgType))
.add("if=" + ifindex)
.add(ip.getHostAddress())
.add(StructNdMsg.stringForNudState(nudState))
- .add("[" + NetlinkConstants.hexify(linkLayerAddr) + "]")
+ .add("[" + macAddr + "]")
.toString();
}
}
@@ -183,7 +185,7 @@
final NetlinkMessage nlMsg = NetlinkMessage.parse(byteBuffer);
if (nlMsg == null || nlMsg.getHeader() == null) {
byteBuffer.position(position);
- mLog.e("unparsable netlink msg: " + NetlinkConstants.hexify(byteBuffer));
+ mLog.e("unparsable netlink msg: " + hexify(byteBuffer));
break;
}
@@ -217,12 +219,13 @@
final int ifindex = ndMsg.ndm_ifindex;
final InetAddress destination = neighMsg.getDestination();
final short nudState =
- (msgType == NetlinkConstants.RTM_DELNEIGH)
+ (msgType == RTM_DELNEIGH)
? StructNdMsg.NUD_NONE
: ndMsg.ndm_state;
final NeighborEvent event = new NeighborEvent(
- whenMs, msgType, ifindex, destination, nudState, neighMsg.getLinkLayerAddress());
+ whenMs, msgType, ifindex, destination, nudState,
+ getMacAddress(neighMsg.getLinkLayerAddress()));
if (VDBG) {
Log.d(TAG, neighMsg.toString());
@@ -233,4 +236,16 @@
mConsumer.accept(event);
}
+
+ private static MacAddress getMacAddress(byte[] linkLayerAddress) {
+ if (linkLayerAddress != null) {
+ try {
+ return MacAddress.fromBytes(linkLayerAddress);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed to parse link-layer address: " + hexify(linkLayerAddress));
+ }
+ }
+
+ return null;
+ }
}
diff --git a/android/net/ip/IpReachabilityMonitor.java b/android/net/ip/IpReachabilityMonitor.java
index b31ffbb..7e02a28 100644
--- a/android/net/ip/IpReachabilityMonitor.java
+++ b/android/net/ip/IpReachabilityMonitor.java
@@ -26,6 +26,7 @@
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.IpReachabilityEvent;
import android.net.netlink.StructNdMsg;
+import android.net.util.InterfaceParams;
import android.net.util.MultinetworkPolicyTracker;
import android.net.util.SharedLog;
import android.os.Handler;
@@ -46,9 +47,7 @@
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
-import java.net.NetworkInterface;
import java.net.SocketAddress;
-import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
@@ -168,8 +167,7 @@
}
}
- private final String mInterfaceName;
- private final int mInterfaceIndex;
+ private final InterfaceParams mInterfaceParams;
private final IpNeighborMonitor mIpNeighborMonitor;
private final SharedLog mLog;
private final Callback mCallback;
@@ -182,30 +180,25 @@
private volatile long mLastProbeTimeMs;
public IpReachabilityMonitor(
- Context context, String ifName, Handler h, SharedLog log, Callback callback) {
- this(context, ifName, h, log, callback, null);
- }
-
- public IpReachabilityMonitor(
- Context context, String ifName, Handler h, SharedLog log, Callback callback,
+ Context context, InterfaceParams ifParams, Handler h, SharedLog log, Callback callback,
MultinetworkPolicyTracker tracker) {
- this(ifName, getInterfaceIndex(ifName), h, log, callback, tracker,
- Dependencies.makeDefault(context, ifName));
+ this(ifParams, h, log, callback, tracker, Dependencies.makeDefault(context, ifParams.name));
}
@VisibleForTesting
- IpReachabilityMonitor(String ifName, int ifIndex, Handler h, SharedLog log, Callback callback,
+ IpReachabilityMonitor(InterfaceParams ifParams, Handler h, SharedLog log, Callback callback,
MultinetworkPolicyTracker tracker, Dependencies dependencies) {
- mInterfaceName = ifName;
+ if (ifParams == null) throw new IllegalArgumentException("null InterfaceParams");
+
+ mInterfaceParams = ifParams;
mLog = log.forSubComponent(TAG);
mCallback = callback;
mMultinetworkPolicyTracker = tracker;
- mInterfaceIndex = ifIndex;
mDependencies = dependencies;
mIpNeighborMonitor = new IpNeighborMonitor(h, mLog,
(NeighborEvent event) -> {
- if (mInterfaceIndex != event.ifindex) return;
+ if (mInterfaceParams.index != event.ifindex) return;
if (!mNeighborWatchList.containsKey(event.ip)) return;
final NeighborEvent prev = mNeighborWatchList.put(event.ip, event);
@@ -241,7 +234,7 @@
private String describeWatchList(String sep) {
final StringBuilder sb = new StringBuilder();
- sb.append("iface{" + mInterfaceName + "/" + mInterfaceIndex + "}," + sep);
+ sb.append("iface{" + mInterfaceParams + "}," + sep);
sb.append("ntable=[" + sep);
String delimiter = "";
for (Map.Entry<InetAddress, NeighborEvent> entry : mNeighborWatchList.entrySet()) {
@@ -262,10 +255,10 @@
}
public void updateLinkProperties(LinkProperties lp) {
- if (!mInterfaceName.equals(lp.getInterfaceName())) {
+ if (!mInterfaceParams.name.equals(lp.getInterfaceName())) {
// TODO: figure out whether / how to cope with interface changes.
Log.wtf(TAG, "requested LinkProperties interface '" + lp.getInterfaceName() +
- "' does not match: " + mInterfaceName);
+ "' does not match: " + mInterfaceParams.name);
return;
}
@@ -353,10 +346,10 @@
mDependencies.acquireWakeLock(getProbeWakeLockDuration());
}
- for (InetAddress target : ipProbeList) {
- final int rval = IpNeighborMonitor.startKernelNeighborProbe(mInterfaceIndex, target);
+ for (InetAddress ip : ipProbeList) {
+ final int rval = IpNeighborMonitor.startKernelNeighborProbe(mInterfaceParams.index, ip);
mLog.log(String.format("put neighbor %s into NUD_PROBE state (rval=%d)",
- target.getHostAddress(), rval));
+ ip.getHostAddress(), rval));
logEvent(IpReachabilityEvent.PROBE, rval);
}
mLastProbeTimeMs = SystemClock.elapsedRealtime();
@@ -378,22 +371,9 @@
return (numUnicastProbes * retransTimeMs) + gracePeriodMs;
}
- private static int getInterfaceIndex(String ifname) {
- final NetworkInterface iface;
- try {
- iface = NetworkInterface.getByName(ifname);
- } catch (SocketException e) {
- throw new IllegalArgumentException("invalid interface '" + ifname + "': ", e);
- }
- if (iface == null) {
- throw new IllegalArgumentException("NetworkInterface was null for " + ifname);
- }
- return iface.getIndex();
- }
-
private void logEvent(int probeType, int errorCode) {
int eventType = probeType | (errorCode & 0xff);
- mMetricsLog.log(mInterfaceName, new IpReachabilityEvent(eventType));
+ mMetricsLog.log(mInterfaceParams.name, new IpReachabilityEvent(eventType));
}
private void logNudFailed(ProvisioningChange delta) {
@@ -401,6 +381,6 @@
boolean isFromProbe = (duration < getProbeWakeLockDuration());
boolean isProvisioningLost = (delta == ProvisioningChange.LOST_PROVISIONING);
int eventType = IpReachabilityEvent.nudFailureEventType(isFromProbe, isProvisioningLost);
- mMetricsLog.log(mInterfaceName, new IpReachabilityEvent(eventType));
+ mMetricsLog.log(mInterfaceParams.name, new IpReachabilityEvent(eventType));
}
}
diff --git a/android/net/ip/RouterAdvertisementDaemon.java b/android/net/ip/RouterAdvertisementDaemon.java
index cb3123c..49a1e79 100644
--- a/android/net/ip/RouterAdvertisementDaemon.java
+++ b/android/net/ip/RouterAdvertisementDaemon.java
@@ -25,6 +25,7 @@
import android.net.LinkProperties;
import android.net.NetworkUtils;
import android.net.TrafficStats;
+import android.net.util.InterfaceParams;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructGroupReq;
@@ -96,9 +97,7 @@
(byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
};
- private final String mIfName;
- private final int mIfIndex;
- private final byte[] mHwAddr;
+ private final InterfaceParams mInterface;
private final InetSocketAddress mAllNodes;
// This lock is to protect the RA from being updated while being
@@ -223,11 +222,9 @@
}
- public RouterAdvertisementDaemon(String ifname, int ifindex, byte[] hwaddr) {
- mIfName = ifname;
- mIfIndex = ifindex;
- mHwAddr = hwaddr;
- mAllNodes = new InetSocketAddress(getAllNodesForScopeId(mIfIndex), 0);
+ public RouterAdvertisementDaemon(InterfaceParams ifParams) {
+ mInterface = ifParams;
+ mAllNodes = new InetSocketAddress(getAllNodesForScopeId(mInterface.index), 0);
mDeprecatedInfoTracker = new DeprecatedInfoTracker();
}
@@ -279,7 +276,7 @@
try {
putHeader(ra, mRaParams != null && mRaParams.hasDefaultRoute);
- putSlla(ra, mHwAddr);
+ putSlla(ra, mInterface.macAddr.toByteArray());
mRaLength = ra.position();
// https://tools.ietf.org/html/rfc5175#section-4 says:
@@ -579,9 +576,9 @@
// Setting SNDTIMEO is purely for defensive purposes.
Os.setsockoptTimeval(
mSocket, SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(SEND_TIMEOUT_MS));
- Os.setsockoptIfreq(mSocket, SOL_SOCKET, SO_BINDTODEVICE, mIfName);
+ Os.setsockoptIfreq(mSocket, SOL_SOCKET, SO_BINDTODEVICE, mInterface.name);
NetworkUtils.protectFromVpn(mSocket);
- NetworkUtils.setupRaSocket(mSocket, mIfIndex);
+ NetworkUtils.setupRaSocket(mSocket, mInterface.index);
} catch (ErrnoException | IOException e) {
Log.e(TAG, "Failed to create RA daemon socket: " + e);
return false;
@@ -614,7 +611,7 @@
final InetAddress destip = dest.getAddress();
return (destip instanceof Inet6Address) &&
destip.isLinkLocalAddress() &&
- (((Inet6Address) destip).getScopeId() == mIfIndex);
+ (((Inet6Address) destip).getScopeId() == mInterface.index);
}
private void maybeSendRA(InetSocketAddress dest) {
diff --git a/android/net/metrics/WakeupStats.java b/android/net/metrics/WakeupStats.java
index 7277ba3..bb36536 100644
--- a/android/net/metrics/WakeupStats.java
+++ b/android/net/metrics/WakeupStats.java
@@ -80,7 +80,7 @@
break;
}
- switch (ev.dstHwAddr.addressType()) {
+ switch (ev.dstHwAddr.getAddressType()) {
case MacAddress.TYPE_UNICAST:
l2UnicastCount++;
break;
diff --git a/android/net/util/ConnectivityPacketSummary.java b/android/net/util/ConnectivityPacketSummary.java
index dae93af..4951400 100644
--- a/android/net/util/ConnectivityPacketSummary.java
+++ b/android/net/util/ConnectivityPacketSummary.java
@@ -17,6 +17,7 @@
package android.net.util;
import android.net.dhcp.DhcpPacket;
+import android.net.MacAddress;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -45,21 +46,20 @@
private final ByteBuffer mPacket;
private final String mSummary;
- public static String summarize(byte[] hwaddr, byte[] buffer) {
+ public static String summarize(MacAddress hwaddr, byte[] buffer) {
return summarize(hwaddr, buffer, buffer.length);
}
// Methods called herein perform some but by no means all error checking.
// They may throw runtime exceptions on malformed packets.
- public static String summarize(byte[] hwaddr, byte[] buffer, int length) {
- if ((hwaddr == null) || (hwaddr.length != ETHER_ADDR_LEN)) return null;
- if (buffer == null) return null;
+ public static String summarize(MacAddress macAddr, byte[] buffer, int length) {
+ if ((macAddr == null) || (buffer == null)) return null;
length = Math.min(length, buffer.length);
- return (new ConnectivityPacketSummary(hwaddr, buffer, length)).toString();
+ return (new ConnectivityPacketSummary(macAddr, buffer, length)).toString();
}
- private ConnectivityPacketSummary(byte[] hwaddr, byte[] buffer, int length) {
- mHwAddr = hwaddr;
+ private ConnectivityPacketSummary(MacAddress macAddr, byte[] buffer, int length) {
+ mHwAddr = macAddr.toByteArray();
mBytes = buffer;
mLength = Math.min(length, mBytes.length);
mPacket = ByteBuffer.wrap(mBytes, 0, mLength);
diff --git a/android/net/util/InterfaceParams.java b/android/net/util/InterfaceParams.java
new file mode 100644
index 0000000..a4b2fbb
--- /dev/null
+++ b/android/net/util/InterfaceParams.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2017 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.net.util;
+
+import static android.net.MacAddress.ALL_ZEROS_ADDRESS;
+import static android.net.util.NetworkConstants.ETHER_MTU;
+import static android.net.util.NetworkConstants.IPV6_MIN_MTU;
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import android.net.MacAddress;
+import android.text.TextUtils;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
+
+
+/**
+ * Encapsulate the interface parameters common to IpClient/IpServer components.
+ *
+ * Basically all java.net.NetworkInterface methods throw Exceptions. IpClient
+ * and IpServer (sub)components need most or all of this information at some
+ * point during their lifecycles, so pass only this simplified object around
+ * which can be created once when IpClient/IpServer are told to start.
+ *
+ * @hide
+ */
+public class InterfaceParams {
+ public final String name;
+ public final int index;
+ public final MacAddress macAddr;
+ public final int defaultMtu;
+
+ public static InterfaceParams getByName(String name) {
+ final NetworkInterface netif = getNetworkInterfaceByName(name);
+ if (netif == null) return null;
+
+ // Not all interfaces have MAC addresses, e.g. rmnet_data0.
+ final MacAddress macAddr = getMacAddress(netif);
+
+ try {
+ return new InterfaceParams(name, netif.getIndex(), macAddr, netif.getMTU());
+ } catch (IllegalArgumentException|SocketException e) {
+ return null;
+ }
+ }
+
+ public InterfaceParams(String name, int index, MacAddress macAddr) {
+ this(name, index, macAddr, ETHER_MTU);
+ }
+
+ public InterfaceParams(String name, int index, MacAddress macAddr, int defaultMtu) {
+ checkArgument((!TextUtils.isEmpty(name)), "impossible interface name");
+ checkArgument((index > 0), "invalid interface index");
+ this.name = name;
+ this.index = index;
+ this.macAddr = (macAddr != null) ? macAddr : ALL_ZEROS_ADDRESS;
+ this.defaultMtu = (defaultMtu > IPV6_MIN_MTU) ? defaultMtu : IPV6_MIN_MTU;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s/%d/%s/%d", name, index, macAddr, defaultMtu);
+ }
+
+ private static NetworkInterface getNetworkInterfaceByName(String name) {
+ try {
+ return NetworkInterface.getByName(name);
+ } catch (NullPointerException|SocketException e) {
+ return null;
+ }
+ }
+
+ private static MacAddress getMacAddress(NetworkInterface netif) {
+ try {
+ return MacAddress.fromBytes(netif.getHardwareAddress());
+ } catch (IllegalArgumentException|NullPointerException|SocketException e) {
+ return null;
+ }
+ }
+}
diff --git a/android/net/util/MultinetworkPolicyTracker.java b/android/net/util/MultinetworkPolicyTracker.java
index 424e40d..30c5cd9 100644
--- a/android/net/util/MultinetworkPolicyTracker.java
+++ b/android/net/util/MultinetworkPolicyTracker.java
@@ -122,6 +122,7 @@
return mAvoidBadWifi;
}
+ // TODO: move this to MultipathPolicyTracker.
public int getMeteredMultipathPreference() {
return mMeteredMultipathPreference;
}
diff --git a/android/net/util/NetworkConstants.java b/android/net/util/NetworkConstants.java
index 5a3a8be..984c9f8 100644
--- a/android/net/util/NetworkConstants.java
+++ b/android/net/util/NetworkConstants.java
@@ -121,6 +121,14 @@
public static final int ICMP_ECHO_DATA_OFFSET = 8;
/**
+ * ICMPv4 constants.
+ *
+ * See also:
+ * - https://tools.ietf.org/html/rfc792
+ */
+ public static final int ICMPV4_ECHO_REQUEST_TYPE = 8;
+
+ /**
* ICMPv6 constants.
*
* See also:
@@ -139,6 +147,8 @@
public static final int ICMPV6_ND_OPTION_TLLA = 2;
public static final int ICMPV6_ND_OPTION_MTU = 5;
+ public static final int ICMPV6_ECHO_REQUEST_TYPE = 128;
+
/**
* UDP constants.
*
@@ -157,6 +167,14 @@
public static final int DHCP4_CLIENT_PORT = 68;
/**
+ * DNS constants.
+ *
+ * See also:
+ * - https://tools.ietf.org/html/rfc1035
+ */
+ public static final int DNS_SERVER_PORT = 53;
+
+ /**
* Utility functions.
*/
public static byte asByte(int i) { return (byte) i; }
diff --git a/android/net/wifi/ScanResult.java b/android/net/wifi/ScanResult.java
index b6ad926..c46789c 100644
--- a/android/net/wifi/ScanResult.java
+++ b/android/net/wifi/ScanResult.java
@@ -21,7 +21,9 @@
import android.os.Parcelable;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
/**
* Describes information about a detected access point. In addition
@@ -227,6 +229,50 @@
public long seen;
/**
+ * On devices with multiple hardware radio chains, this class provides metadata about
+ * each radio chain that was used to receive this scan result (probe response or beacon).
+ * {@hide}
+ */
+ public static class RadioChainInfo {
+ /** Vendor defined id for a radio chain. */
+ public int id;
+ /** Detected signal level in dBm (also known as the RSSI) on this radio chain. */
+ public int level;
+
+ @Override
+ public String toString() {
+ return "RadioChainInfo: id=" + id + ", level=" + level;
+ }
+
+ @Override
+ public boolean equals(Object otherObj) {
+ if (this == otherObj) {
+ return true;
+ }
+ if (!(otherObj instanceof RadioChainInfo)) {
+ return false;
+ }
+ RadioChainInfo other = (RadioChainInfo) otherObj;
+ return id == other.id && level == other.level;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, level);
+ }
+ };
+
+ /**
+ * Information about the list of the radio chains used to receive this scan result
+ * (probe response or beacon).
+ *
+ * For Example: On devices with 2 hardware radio chains, this list could hold 1 or 2
+ * entries based on whether this scan result was received using one or both the chains.
+ * {@hide}
+ */
+ public RadioChainInfo[] radioChainInfos;
+
+ /**
* @hide
* Update RSSI of the scan result
* @param previousRssi
@@ -248,18 +294,6 @@
}
/**
- * num IP configuration failures
- * @hide
- */
- public int numIpConfigFailures;
-
- /**
- * @hide
- * Last time we blacklisted the ScanResult
- */
- public long blackListTimestamp;
-
- /**
* Status indicating the scan result does not correspond to a user's saved configuration
* @hide
* @removed
@@ -268,12 +302,6 @@
public boolean untrusted;
/**
- * Number of time we connected to it
- * @hide
- */
- public int numConnection;
-
- /**
* Number of time autojoin used it
* @hide
*/
@@ -386,12 +414,6 @@
*/
public List<String> anqpLines;
- /**
- * @hide
- * storing the raw bytes of full result IEs
- **/
- public byte[] bytes;
-
/** information elements from beacon
* @hide
*/
@@ -481,6 +503,7 @@
this.isCarrierAp = false;
this.carrierApEapType = UNSPECIFIED;
this.carrierName = null;
+ this.radioChainInfos = null;
}
/** {@hide} */
@@ -502,6 +525,7 @@
this.isCarrierAp = false;
this.carrierApEapType = UNSPECIFIED;
this.carrierName = null;
+ this.radioChainInfos = null;
}
/** {@hide} */
@@ -530,6 +554,7 @@
this.isCarrierAp = false;
this.carrierApEapType = UNSPECIFIED;
this.carrierName = null;
+ this.radioChainInfos = null;
}
/** {@hide} */
@@ -563,15 +588,14 @@
distanceSdCm = source.distanceSdCm;
seen = source.seen;
untrusted = source.untrusted;
- numConnection = source.numConnection;
numUsage = source.numUsage;
- numIpConfigFailures = source.numIpConfigFailures;
venueName = source.venueName;
operatorFriendlyName = source.operatorFriendlyName;
flags = source.flags;
isCarrierAp = source.isCarrierAp;
carrierApEapType = source.carrierApEapType;
carrierName = source.carrierName;
+ radioChainInfos = source.radioChainInfos;
}
}
@@ -615,6 +639,7 @@
sb.append(", Carrier AP: ").append(isCarrierAp ? "yes" : "no");
sb.append(", Carrier AP EAP Type: ").append(carrierApEapType);
sb.append(", Carrier name: ").append(carrierName);
+ sb.append(", Radio Chain Infos: ").append(Arrays.toString(radioChainInfos));
return sb.toString();
}
@@ -646,9 +671,7 @@
dest.writeInt(centerFreq1);
dest.writeLong(seen);
dest.writeInt(untrusted ? 1 : 0);
- dest.writeInt(numConnection);
dest.writeInt(numUsage);
- dest.writeInt(numIpConfigFailures);
dest.writeString((venueName != null) ? venueName.toString() : "");
dest.writeString((operatorFriendlyName != null) ? operatorFriendlyName.toString() : "");
dest.writeLong(this.flags);
@@ -687,6 +710,16 @@
dest.writeInt(isCarrierAp ? 1 : 0);
dest.writeInt(carrierApEapType);
dest.writeString(carrierName);
+
+ if (radioChainInfos != null) {
+ dest.writeInt(radioChainInfos.length);
+ for (int i = 0; i < radioChainInfos.length; i++) {
+ dest.writeInt(radioChainInfos[i].id);
+ dest.writeInt(radioChainInfos[i].level);
+ }
+ } else {
+ dest.writeInt(0);
+ }
}
/** Implement the Parcelable interface {@hide} */
@@ -718,9 +751,7 @@
sr.seen = in.readLong();
sr.untrusted = in.readInt() != 0;
- sr.numConnection = in.readInt();
sr.numUsage = in.readInt();
- sr.numIpConfigFailures = in.readInt();
sr.venueName = in.readString();
sr.operatorFriendlyName = in.readString();
sr.flags = in.readLong();
@@ -759,6 +790,15 @@
sr.isCarrierAp = in.readInt() != 0;
sr.carrierApEapType = in.readInt();
sr.carrierName = in.readString();
+ n = in.readInt();
+ if (n != 0) {
+ sr.radioChainInfos = new RadioChainInfo[n];
+ for (int i = 0; i < n; i++) {
+ sr.radioChainInfos[i] = new RadioChainInfo();
+ sr.radioChainInfos[i].id = in.readInt();
+ sr.radioChainInfos[i].level = in.readInt();
+ }
+ }
return sr;
}
diff --git a/android/net/wifi/WifiActivityEnergyInfo.java b/android/net/wifi/WifiActivityEnergyInfo.java
index 29bf02c..03c9fbe 100644
--- a/android/net/wifi/WifiActivityEnergyInfo.java
+++ b/android/net/wifi/WifiActivityEnergyInfo.java
@@ -56,6 +56,11 @@
/**
* @hide
*/
+ public long mControllerScanTimeMs;
+
+ /**
+ * @hide
+ */
public long mControllerIdleTimeMs;
/**
@@ -69,13 +74,14 @@
public static final int STACK_STATE_STATE_IDLE = 3;
public WifiActivityEnergyInfo(long timestamp, int stackState,
- long txTime, long[] txTimePerLevel, long rxTime, long idleTime,
- long energyUsed) {
+ long txTime, long[] txTimePerLevel, long rxTime, long scanTime,
+ long idleTime, long energyUsed) {
mTimestamp = timestamp;
mStackState = stackState;
mControllerTxTimeMs = txTime;
mControllerTxTimePerLevelMs = txTimePerLevel;
mControllerRxTimeMs = rxTime;
+ mControllerScanTimeMs = scanTime;
mControllerIdleTimeMs = idleTime;
mControllerEnergyUsed = energyUsed;
}
@@ -88,6 +94,7 @@
+ " mControllerTxTimeMs=" + mControllerTxTimeMs
+ " mControllerTxTimePerLevelMs=" + Arrays.toString(mControllerTxTimePerLevelMs)
+ " mControllerRxTimeMs=" + mControllerRxTimeMs
+ + " mControllerScanTimeMs=" + mControllerScanTimeMs
+ " mControllerIdleTimeMs=" + mControllerIdleTimeMs
+ " mControllerEnergyUsed=" + mControllerEnergyUsed
+ " }";
@@ -101,10 +108,11 @@
long txTime = in.readLong();
long[] txTimePerLevel = in.createLongArray();
long rxTime = in.readLong();
+ long scanTime = in.readLong();
long idleTime = in.readLong();
long energyUsed = in.readLong();
return new WifiActivityEnergyInfo(timestamp, stackState,
- txTime, txTimePerLevel, rxTime, idleTime, energyUsed);
+ txTime, txTimePerLevel, rxTime, scanTime, idleTime, energyUsed);
}
public WifiActivityEnergyInfo[] newArray(int size) {
return new WifiActivityEnergyInfo[size];
@@ -117,6 +125,7 @@
out.writeLong(mControllerTxTimeMs);
out.writeLongArray(mControllerTxTimePerLevelMs);
out.writeLong(mControllerRxTimeMs);
+ out.writeLong(mControllerScanTimeMs);
out.writeLong(mControllerIdleTimeMs);
out.writeLong(mControllerEnergyUsed);
}
@@ -157,6 +166,13 @@
}
/**
+ * @return scan time in ms
+ */
+ public long getControllerScanTimeMillis() {
+ return mControllerScanTimeMs;
+ }
+
+ /**
* @return idle time in ms
*/
public long getControllerIdleTimeMillis() {
@@ -183,6 +199,7 @@
public boolean isValid() {
return ((mControllerTxTimeMs >=0) &&
(mControllerRxTimeMs >=0) &&
+ (mControllerScanTimeMs >=0) &&
(mControllerIdleTimeMs >=0));
}
-}
+}
\ No newline at end of file
diff --git a/android/net/wifi/WifiConfiguration.java b/android/net/wifi/WifiConfiguration.java
index 6438631..8d1a00b 100644
--- a/android/net/wifi/WifiConfiguration.java
+++ b/android/net/wifi/WifiConfiguration.java
@@ -20,6 +20,7 @@
import android.content.pm.PackageManager;
import android.net.IpConfiguration;
import android.net.IpConfiguration.ProxySettings;
+import android.net.MacAddress;
import android.net.ProxyInfo;
import android.net.StaticIpConfiguration;
import android.net.Uri;
@@ -54,8 +55,10 @@
/** {@hide} */
public static final String pskVarName = "psk";
/** {@hide} */
+ @Deprecated
public static final String[] wepKeyVarNames = { "wep_key0", "wep_key1", "wep_key2", "wep_key3" };
/** {@hide} */
+ @Deprecated
public static final String wepTxKeyIdxVarName = "wep_tx_keyidx";
/** {@hide} */
public static final String priorityVarName = "priority";
@@ -82,6 +85,9 @@
/** WPA is not used; plaintext or static WEP could be used. */
public static final int NONE = 0;
/** WPA pre-shared key (requires {@code preSharedKey} to be specified). */
+ /** @deprecated Due to security and performance limitations, use of WPA-1 networks
+ * is discouraged. WPA-2 (RSN) should be used instead. */
+ @Deprecated
public static final int WPA_PSK = 1;
/** WPA using EAP authentication. Generally used with an external authentication server. */
public static final int WPA_EAP = 2;
@@ -115,8 +121,8 @@
public static final String varName = "key_mgmt";
- public static final String[] strings = { "NONE", "WPA_PSK", "WPA_EAP", "IEEE8021X",
- "WPA2_PSK", "OSEN", "FT_PSK", "FT_EAP" };
+ public static final String[] strings = { "NONE", /* deprecated */ "WPA_PSK", "WPA_EAP",
+ "IEEE8021X", "WPA2_PSK", "OSEN", "FT_PSK", "FT_EAP" };
}
/**
@@ -125,7 +131,10 @@
public static class Protocol {
private Protocol() { }
- /** WPA/IEEE 802.11i/D3.0 */
+ /** WPA/IEEE 802.11i/D3.0
+ * @deprecated Due to security and performance limitations, use of WPA-1 networks
+ * is discouraged. WPA-2 (RSN) should be used instead. */
+ @Deprecated
public static final int WPA = 0;
/** WPA2/IEEE 802.11i */
public static final int RSN = 1;
@@ -147,7 +156,10 @@
/** Open System authentication (required for WPA/WPA2) */
public static final int OPEN = 0;
- /** Shared Key authentication (requires static WEP keys) */
+ /** Shared Key authentication (requires static WEP keys)
+ * @deprecated Due to security and performance limitations, use of WEP networks
+ * is discouraged. */
+ @Deprecated
public static final int SHARED = 1;
/** LEAP/Network EAP (only used with LEAP) */
public static final int LEAP = 2;
@@ -165,7 +177,10 @@
/** Use only Group keys (deprecated) */
public static final int NONE = 0;
- /** Temporal Key Integrity Protocol [IEEE 802.11i/D7.0] */
+ /** Temporal Key Integrity Protocol [IEEE 802.11i/D7.0]
+ * @deprecated Due to security and performance limitations, use of WPA-1 networks
+ * is discouraged. WPA-2 (RSN) should be used instead. */
+ @Deprecated
public static final int TKIP = 1;
/** AES in Counter mode with CBC-MAC [RFC 3610, IEEE 802.11i/D7.0] */
public static final int CCMP = 2;
@@ -187,9 +202,15 @@
public static class GroupCipher {
private GroupCipher() { }
- /** WEP40 = WEP (Wired Equivalent Privacy) with 40-bit key (original 802.11) */
+ /** WEP40 = WEP (Wired Equivalent Privacy) with 40-bit key (original 802.11)
+ * @deprecated Due to security and performance limitations, use of WEP networks
+ * is discouraged. */
+ @Deprecated
public static final int WEP40 = 0;
- /** WEP104 = WEP (Wired Equivalent Privacy) with 104-bit key */
+ /** WEP104 = WEP (Wired Equivalent Privacy) with 104-bit key
+ * @deprecated Due to security and performance limitations, use of WEP networks
+ * is discouraged. */
+ @Deprecated
public static final int WEP104 = 1;
/** Temporal Key Integrity Protocol [IEEE 802.11i/D7.0] */
public static final int TKIP = 2;
@@ -203,7 +224,8 @@
public static final String varName = "group";
public static final String[] strings =
- { "WEP40", "WEP104", "TKIP", "CCMP", "GTK_NOT_USED" };
+ { /* deprecated */ "WEP40", /* deprecated */ "WEP104",
+ "TKIP", "CCMP", "GTK_NOT_USED" };
}
/** Possible status of a network configuration. */
@@ -267,8 +289,15 @@
public static final int AP_BAND_5GHZ = 1;
/**
+ * Device is allowed to choose the optimal band (2Ghz or 5Ghz) based on device capability,
+ * operating country code and current radio conditions.
+ * @hide
+ */
+ public static final int AP_BAND_ANY = -1;
+
+ /**
* The band which AP resides on
- * 0-2G 1-5G
+ * -1:Any 0:2G 1:5G
* By default, 2G is chosen
* @hide
*/
@@ -302,10 +331,16 @@
* When the value of one of these keys is read, the actual key is
* not returned, just a "*" if the key has a value, or the null
* string otherwise.
+ * @deprecated Due to security and performance limitations, use of WEP networks
+ * is discouraged.
*/
+ @Deprecated
public String[] wepKeys;
- /** Default WEP key index, ranging from 0 to 3. */
+ /** Default WEP key index, ranging from 0 to 3.
+ * @deprecated Due to security and performance limitations, use of WEP networks
+ * is discouraged. */
+ @Deprecated
public int wepTxKeyIndex;
/**
@@ -845,6 +880,52 @@
@SystemApi
public int numAssociation;
+ /**
+ * @hide
+ * Randomized MAC address to use with this particular network
+ */
+ private MacAddress mRandomizedMacAddress;
+
+ /**
+ * @hide
+ * Checks if the given MAC address can be used for Connected Mac Randomization
+ * by verifying that it is non-null, unicast, and locally assigned.
+ * @param mac MacAddress to check
+ * @return true if mac is good to use
+ */
+ private boolean isValidMacAddressForRandomization(MacAddress mac) {
+ return mac != null && !mac.isMulticastAddress() && mac.isLocallyAssigned();
+ }
+
+ /**
+ * @hide
+ * Returns Randomized MAC address to use with the network.
+ * If it is not set/valid, create a new randomized address.
+ */
+ public MacAddress getOrCreateRandomizedMacAddress() {
+ if (!isValidMacAddressForRandomization(mRandomizedMacAddress)) {
+ mRandomizedMacAddress = MacAddress.createRandomUnicastAddress();
+ }
+ return mRandomizedMacAddress;
+ }
+
+ /**
+ * @hide
+ * Returns MAC address set to be the local randomized MAC address.
+ * Does not guarantee that the returned address is valid for use.
+ */
+ public MacAddress getRandomizedMacAddress() {
+ return mRandomizedMacAddress;
+ }
+
+ /**
+ * @hide
+ * @param mac MacAddress to change into
+ */
+ public void setRandomizedMacAddress(MacAddress mac) {
+ mRandomizedMacAddress = mac;
+ }
+
/** @hide
* Boost given to RSSI on a home network for the purpose of calculating the score
* This adds stickiness to home networks, as defined by:
@@ -2117,6 +2198,7 @@
updateTime = source.updateTime;
shared = source.shared;
recentFailure.setAssociationStatus(source.recentFailure.getAssociationStatus());
+ mRandomizedMacAddress = source.mRandomizedMacAddress;
}
}
@@ -2184,6 +2266,7 @@
dest.writeInt(shared ? 1 : 0);
dest.writeString(mPasspointManagementObjectTree);
dest.writeInt(recentFailure.getAssociationStatus());
+ dest.writeParcelable(mRandomizedMacAddress, flags);
}
/** Implement the Parcelable interface {@hide} */
@@ -2252,6 +2335,7 @@
config.shared = in.readInt() != 0;
config.mPasspointManagementObjectTree = in.readString();
config.recentFailure.setAssociationStatus(in.readInt());
+ config.mRandomizedMacAddress = in.readParcelable(null);
return config;
}
diff --git a/android/net/wifi/WifiConnectionStatistics.java b/android/net/wifi/WifiConnectionStatistics.java
deleted file mode 100644
index 1120c66..0000000
--- a/android/net/wifi/WifiConnectionStatistics.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright (C) 2014 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.net.wifi;
-
-import android.annotation.SystemApi;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.text.TextUtils;
-
-import java.util.HashMap;
-
-/**
- * Wifi Connection Statistics: gather various stats regarding WiFi connections,
- * connection requests, auto-join
- * and WiFi usage.
- * @hide
- * @removed
- */
-@SystemApi
-public class WifiConnectionStatistics implements Parcelable {
- private static final String TAG = "WifiConnnectionStatistics";
-
- /**
- * history of past connection to untrusted SSID
- * Key = SSID
- * Value = num connection
- */
- public HashMap<String, WifiNetworkConnectionStatistics> untrustedNetworkHistory;
-
- // Number of time we polled the chip and were on 5GHz
- public int num5GhzConnected;
-
- // Number of time we polled the chip and were on 2.4GHz
- public int num24GhzConnected;
-
- // Number autojoin attempts
- public int numAutoJoinAttempt;
-
- // Number auto-roam attempts
- public int numAutoRoamAttempt;
-
- // Number wifimanager join attempts
- public int numWifiManagerJoinAttempt;
-
- public WifiConnectionStatistics() {
- untrustedNetworkHistory = new HashMap<String, WifiNetworkConnectionStatistics>();
- }
-
- public void incrementOrAddUntrusted(String SSID, int connection, int usage) {
- WifiNetworkConnectionStatistics stats;
- if (TextUtils.isEmpty(SSID))
- return;
- if (untrustedNetworkHistory.containsKey(SSID)) {
- stats = untrustedNetworkHistory.get(SSID);
- if (stats != null){
- stats.numConnection = connection + stats.numConnection;
- stats.numUsage = usage + stats.numUsage;
- }
- } else {
- stats = new WifiNetworkConnectionStatistics(connection, usage);
- }
- if (stats != null) {
- untrustedNetworkHistory.put(SSID, stats);
- }
- }
-
- @Override
- public String toString() {
- StringBuilder sbuf = new StringBuilder();
- sbuf.append("Connected on: 2.4Ghz=").append(num24GhzConnected);
- sbuf.append(" 5Ghz=").append(num5GhzConnected).append("\n");
- sbuf.append(" join=").append(numWifiManagerJoinAttempt);
- sbuf.append("\\").append(numAutoJoinAttempt).append("\n");
- sbuf.append(" roam=").append(numAutoRoamAttempt).append("\n");
-
- for (String Key : untrustedNetworkHistory.keySet()) {
- WifiNetworkConnectionStatistics stats = untrustedNetworkHistory.get(Key);
- if (stats != null) {
- sbuf.append(Key).append(" ").append(stats.toString()).append("\n");
- }
- }
- return sbuf.toString();
- }
-
- /** copy constructor*/
- public WifiConnectionStatistics(WifiConnectionStatistics source) {
- untrustedNetworkHistory = new HashMap<String, WifiNetworkConnectionStatistics>();
- if (source != null) {
- untrustedNetworkHistory.putAll(source.untrustedNetworkHistory);
- }
- }
-
- /** Implement the Parcelable interface */
- public int describeContents() {
- return 0;
- }
-
- /** Implement the Parcelable interface */
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(num24GhzConnected);
- dest.writeInt(num5GhzConnected);
- dest.writeInt(numAutoJoinAttempt);
- dest.writeInt(numAutoRoamAttempt);
- dest.writeInt(numWifiManagerJoinAttempt);
-
- dest.writeInt(untrustedNetworkHistory.size());
- for (String Key : untrustedNetworkHistory.keySet()) {
- WifiNetworkConnectionStatistics num = untrustedNetworkHistory.get(Key);
- dest.writeString(Key);
- dest.writeInt(num.numConnection);
- dest.writeInt(num.numUsage);
-
- }
- }
-
- /** Implement the Parcelable interface */
- public static final Creator<WifiConnectionStatistics> CREATOR =
- new Creator<WifiConnectionStatistics>() {
- public WifiConnectionStatistics createFromParcel(Parcel in) {
- WifiConnectionStatistics stats = new WifiConnectionStatistics();
- stats.num24GhzConnected = in.readInt();
- stats.num5GhzConnected = in.readInt();
- stats.numAutoJoinAttempt = in.readInt();
- stats.numAutoRoamAttempt = in.readInt();
- stats.numWifiManagerJoinAttempt = in.readInt();
- int n = in.readInt();
- while (n-- > 0) {
- String Key = in.readString();
- int numConnection = in.readInt();
- int numUsage = in.readInt();
- WifiNetworkConnectionStatistics st =
- new WifiNetworkConnectionStatistics(numConnection, numUsage);
- stats.untrustedNetworkHistory.put(Key, st);
- }
- return stats;
- }
-
- public WifiConnectionStatistics[] newArray(int size) {
- return new WifiConnectionStatistics[size];
- }
- };
-}
diff --git a/android/net/wifi/WifiManager.java b/android/net/wifi/WifiManager.java
index ea9be29..05dcb33 100644
--- a/android/net/wifi/WifiManager.java
+++ b/android/net/wifi/WifiManager.java
@@ -16,6 +16,7 @@
package android.net.wifi;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -55,6 +56,8 @@
import dalvik.system.CloseGuard;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.net.InetAddress;
import java.util.Collections;
@@ -432,6 +435,17 @@
*/
public static final String EXTRA_WIFI_AP_MODE = "wifi_ap_mode";
+ /** @hide */
+ @IntDef(flag = false, prefix = { "WIFI_AP_STATE_" }, value = {
+ WIFI_AP_STATE_DISABLING,
+ WIFI_AP_STATE_DISABLED,
+ WIFI_AP_STATE_ENABLING,
+ WIFI_AP_STATE_ENABLED,
+ WIFI_AP_STATE_FAILED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface WifiApState {}
+
/**
* Wi-Fi AP is currently being disabled. The state will change to
* {@link #WIFI_AP_STATE_DISABLED} if it finishes successfully.
@@ -486,6 +500,14 @@
@SystemApi
public static final int WIFI_AP_STATE_FAILED = 14;
+ /** @hide */
+ @IntDef(flag = false, prefix = { "SAP_START_FAILURE_" }, value = {
+ SAP_START_FAILURE_GENERAL,
+ SAP_START_FAILURE_NO_CHANNEL,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SapStartFailure {}
+
/**
* If WIFI AP start failed, this reason code means there is no legal channel exists on
* user selected band by regulatory
@@ -557,14 +579,9 @@
public static final String EXTRA_SUPPLICANT_CONNECTED = "connected";
/**
* Broadcast intent action indicating that the state of Wi-Fi connectivity
- * has changed. One extra provides the new state
- * in the form of a {@link android.net.NetworkInfo} object. If the new
- * state is CONNECTED, additional extras may provide the BSSID and WifiInfo of
- * the access point.
- * as a {@code String}.
+ * has changed. An extra provides the new state
+ * in the form of a {@link android.net.NetworkInfo} object.
* @see #EXTRA_NETWORK_INFO
- * @see #EXTRA_BSSID
- * @see #EXTRA_WIFI_INFO
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
public static final String NETWORK_STATE_CHANGED_ACTION = "android.net.wifi.STATE_CHANGE";
@@ -576,17 +593,16 @@
public static final String EXTRA_NETWORK_INFO = "networkInfo";
/**
* The lookup key for a String giving the BSSID of the access point to which
- * we are connected. Only present when the new state is CONNECTED.
- * Retrieve with
- * {@link android.content.Intent#getStringExtra(String)}.
+ * we are connected. No longer used.
*/
+ @Deprecated
public static final String EXTRA_BSSID = "bssid";
/**
* The lookup key for a {@link android.net.wifi.WifiInfo} object giving the
- * information about the access point to which we are connected. Only present
- * when the new state is CONNECTED. Retrieve with
- * {@link android.content.Intent#getParcelableExtra(String)}.
+ * information about the access point to which we are connected.
+ * No longer used.
*/
+ @Deprecated
public static final String EXTRA_WIFI_INFO = "wifiInfo";
/**
* Broadcast intent action indicating that the state of establishing a connection to
@@ -695,11 +711,11 @@
* representing if the scan was successful or not.
* Scans may fail for multiple reasons, these may include:
* <ol>
- * <li>A non-privileged app requested too many scans in a certain period of time.
- * This may lead to additional scan request rejections via "scan throttling".
- * See
- * <a href="https://developer.android.com/preview/features/background-location-limits.html">
- * here</a> for details.
+ * <li>An app requested too many scans in a certain period of time.
+ * This may lead to additional scan request rejections via "scan throttling" for both
+ * foreground and background apps.
+ * Note: Apps holding android.Manifest.permission.NETWORK_SETTINGS permission are
+ * exempted from scan throttling.
* </li>
* <li>The device is idle and scanning is disabled.</li>
* <li>Wifi hardware reported a scan failure.</li>
@@ -1005,20 +1021,6 @@
}
/**
- * @hide
- * @removed
- */
- @SystemApi
- @RequiresPermission(android.Manifest.permission.READ_WIFI_CREDENTIAL)
- public WifiConnectionStatistics getConnectionStatistics() {
- try {
- return mService.getConnectionStatistics();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /**
* Returns a WifiConfiguration matching this ScanResult
*
* @param scanResult scanResult that represents the BSSID
@@ -1130,7 +1132,7 @@
*/
private int addOrUpdateNetwork(WifiConfiguration config) {
try {
- return mService.addOrUpdateNetwork(config);
+ return mService.addOrUpdateNetwork(config, mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1151,7 +1153,7 @@
*/
public void addOrUpdatePasspointConfiguration(PasspointConfiguration config) {
try {
- if (!mService.addOrUpdatePasspointConfiguration(config)) {
+ if (!mService.addOrUpdatePasspointConfiguration(config, mContext.getOpPackageName())) {
throw new IllegalArgumentException();
}
} catch (RemoteException e) {
@@ -1168,7 +1170,7 @@
*/
public void removePasspointConfiguration(String fqdn) {
try {
- if (!mService.removePasspointConfiguration(fqdn)) {
+ if (!mService.removePasspointConfiguration(fqdn, mContext.getOpPackageName())) {
throw new IllegalArgumentException();
}
} catch (RemoteException e) {
@@ -1254,7 +1256,7 @@
*/
public boolean removeNetwork(int netId) {
try {
- return mService.removeNetwork(netId);
+ return mService.removeNetwork(netId, mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1300,7 +1302,7 @@
boolean success;
try {
- success = mService.enableNetwork(netId, attemptConnect);
+ success = mService.enableNetwork(netId, attemptConnect, mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1326,7 +1328,7 @@
*/
public boolean disableNetwork(int netId) {
try {
- return mService.disableNetwork(netId);
+ return mService.disableNetwork(netId, mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1339,7 +1341,7 @@
*/
public boolean disconnect() {
try {
- mService.disconnect();
+ mService.disconnect(mContext.getOpPackageName());
return true;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -1354,7 +1356,7 @@
*/
public boolean reconnect() {
try {
- mService.reconnect();
+ mService.reconnect(mContext.getOpPackageName());
return true;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -1369,7 +1371,7 @@
*/
public boolean reassociate() {
try {
- mService.reassociate();
+ mService.reassociate(mContext.getOpPackageName());
return true;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -1596,7 +1598,10 @@
* {@code ((WifiManager) getSystemService(WIFI_SERVICE)).getScanResults()}</li>
* </ol>
* @return {@code true} if the operation succeeded, i.e., the scan was initiated.
+ * @deprecated The ability for apps to trigger scan requests will be removed in a future
+ * release.
*/
+ @Deprecated
public boolean startScan() {
return startScan(null);
}
@@ -1615,58 +1620,12 @@
}
/**
- * startLocationRestrictedScan()
- * Trigger a scan which will not make use of DFS channels and is thus not suitable for
- * establishing wifi connection.
- * @deprecated This API is nolonger supported.
- * Use {@link android.net.wifi.WifiScanner} API
- * @hide
- * @removed
- */
- @Deprecated
- @SystemApi
- @SuppressLint("Doclava125")
- public boolean startLocationRestrictedScan(WorkSource workSource) {
- return false;
- }
-
- /**
- * Check if the Batched Scan feature is supported.
- *
- * @return false if not supported.
- * @deprecated This API is nolonger supported.
- * Use {@link android.net.wifi.WifiScanner} API
- * @hide
- * @removed
- */
- @Deprecated
- @SystemApi
- @SuppressLint("Doclava125")
- public boolean isBatchedScanSupported() {
- return false;
- }
-
- /**
- * Retrieve the latest batched scan result. This should be called immediately after
- * {@link BATCHED_SCAN_RESULTS_AVAILABLE_ACTION} is received.
- * @deprecated This API is nolonger supported.
- * Use {@link android.net.wifi.WifiScanner} API
- * @hide
- * @removed
- */
- @Deprecated
- @SystemApi
- @SuppressLint("Doclava125")
- public List<BatchedScanResult> getBatchedScanResults() {
- return null;
- }
-
- /**
* Creates a configuration token describing the current network of MIME type
* application/vnd.wfa.wsc. Can be used to configure WiFi networks via NFC.
*
* @return hex-string encoded configuration token or null if there is no current network
* @hide
+ * @deprecated This API is deprecated
*/
public String getCurrentNetworkWpsNfcConfigurationToken() {
try {
@@ -1742,7 +1701,7 @@
@Deprecated
public boolean saveConfiguration() {
try {
- return mService.saveConfiguration();
+ return mService.saveConfiguration(mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -2177,7 +2136,7 @@
@RequiresPermission(android.Manifest.permission.CHANGE_WIFI_STATE)
public boolean setWifiApConfiguration(WifiConfiguration wifiConfig) {
try {
- mService.setWifiApConfiguration(wifiConfig);
+ mService.setWifiApConfiguration(wifiConfig, mContext.getOpPackageName());
return true;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -2252,20 +2211,34 @@
/** @hide */
public static final int SAVE_NETWORK_SUCCEEDED = BASE + 9;
- /** @hide */
+ /** @hide
+ * @deprecated This is deprecated
+ */
public static final int START_WPS = BASE + 10;
- /** @hide */
+ /** @hide
+ * @deprecated This is deprecated
+ */
public static final int START_WPS_SUCCEEDED = BASE + 11;
- /** @hide */
+ /** @hide
+ * @deprecated This is deprecated
+ */
public static final int WPS_FAILED = BASE + 12;
- /** @hide */
+ /** @hide
+ * @deprecated This is deprecated
+ */
public static final int WPS_COMPLETED = BASE + 13;
- /** @hide */
+ /** @hide
+ * @deprecated This is deprecated
+ */
public static final int CANCEL_WPS = BASE + 14;
- /** @hide */
+ /** @hide
+ * @deprecated This is deprecated
+ */
public static final int CANCEL_WPS_FAILED = BASE + 15;
- /** @hide */
+ /** @hide
+ * @deprecated This is deprecated
+ */
public static final int CANCEL_WPS_SUCCEDED = BASE + 16;
/** @hide */
@@ -2305,15 +2278,25 @@
public static final int BUSY = 2;
/* WPS specific errors */
- /** WPS overlap detected */
+ /** WPS overlap detected
+ * @deprecated This is deprecated
+ */
public static final int WPS_OVERLAP_ERROR = 3;
- /** WEP on WPS is prohibited */
+ /** WEP on WPS is prohibited
+ * @deprecated This is deprecated
+ */
public static final int WPS_WEP_PROHIBITED = 4;
- /** TKIP only prohibited */
+ /** TKIP only prohibited
+ * @deprecated This is deprecated
+ */
public static final int WPS_TKIP_ONLY_PROHIBITED = 5;
- /** Authentication failure on WPS */
+ /** Authentication failure on WPS
+ * @deprecated This is deprecated
+ */
public static final int WPS_AUTH_FAILURE = 6;
- /** WPS timed out */
+ /** WPS timed out
+ * @deprecated This is deprecated
+ */
public static final int WPS_TIMED_OUT = 7;
/**
@@ -2354,12 +2337,19 @@
public void onFailure(int reason);
}
- /** Interface for callback invocation on a start WPS action */
+ /** Interface for callback invocation on a start WPS action
+ * @deprecated This is deprecated
+ */
public static abstract class WpsCallback {
- /** WPS start succeeded */
+
+ /** WPS start succeeded
+ * @deprecated This API is deprecated
+ */
public abstract void onStarted(String pin);
- /** WPS operation completed successfully */
+ /** WPS operation completed successfully
+ * @deprecated This API is deprecated
+ */
public abstract void onSucceeded();
/**
@@ -2368,6 +2358,7 @@
* {@link #WPS_TKIP_ONLY_PROHIBITED}, {@link #WPS_OVERLAP_ERROR},
* {@link #WPS_WEP_PROHIBITED}, {@link #WPS_TIMED_OUT} or {@link #WPS_AUTH_FAILURE}
* and some generic errors.
+ * @deprecated This API is deprecated
*/
public abstract void onFailed(int reason);
}
@@ -2388,6 +2379,119 @@
}
/**
+ * Base class for soft AP callback. Should be extended by applications and set when calling
+ * {@link WifiManager#registerSoftApCallback(SoftApCallback, Handler)}.
+ *
+ * @hide
+ */
+ public interface SoftApCallback {
+ /**
+ * Called when soft AP state changes.
+ *
+ * @param state new new AP state. One of {@link #WIFI_AP_STATE_DISABLED},
+ * {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED},
+ * {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED}
+ * @param failureReason reason when in failed state. One of
+ * {@link #SAP_START_FAILURE_GENERAL}, {@link #SAP_START_FAILURE_NO_CHANNEL}
+ */
+ public abstract void onStateChanged(@WifiApState int state,
+ @SapStartFailure int failureReason);
+
+ /**
+ * Called when number of connected clients to soft AP changes.
+ *
+ * @param numClients number of connected clients
+ */
+ public abstract void onNumClientsChanged(int numClients);
+ }
+
+ /**
+ * Callback proxy for SoftApCallback objects.
+ *
+ * @hide
+ */
+ private static class SoftApCallbackProxy extends ISoftApCallback.Stub {
+ private final Handler mHandler;
+ private final SoftApCallback mCallback;
+
+ SoftApCallbackProxy(Looper looper, SoftApCallback callback) {
+ mHandler = new Handler(looper);
+ mCallback = callback;
+ }
+
+ @Override
+ public void onStateChanged(int state, int failureReason) throws RemoteException {
+ Log.v(TAG, "SoftApCallbackProxy: onStateChanged: state=" + state + ", failureReason=" +
+ failureReason);
+ mHandler.post(() -> {
+ mCallback.onStateChanged(state, failureReason);
+ });
+ }
+
+ @Override
+ public void onNumClientsChanged(int numClients) throws RemoteException {
+ Log.v(TAG, "SoftApCallbackProxy: onNumClientsChanged: numClients=" + numClients);
+ mHandler.post(() -> {
+ mCallback.onNumClientsChanged(numClients);
+ });
+ }
+ }
+
+ /**
+ * Registers a callback for Soft AP. See {@link SoftApCallback}. Caller will receive the current
+ * soft AP state and number of connected devices immediately after a successful call to this API
+ * via callback. Note that receiving an immediate WIFI_AP_STATE_FAILED value for soft AP state
+ * indicates that the latest attempt to start soft AP has failed. Caller can unregister a
+ * previously registered callback using {@link unregisterSoftApCallback}
+ * <p>
+ * Applications should have the
+ * {@link android.Manifest.permission#NETWORK_SETTINGS NETWORK_SETTINGS} permission. Callers
+ * without the permission will trigger a {@link java.lang.SecurityException}.
+ * <p>
+ *
+ * @param callback Callback for soft AP events
+ * @param handler The Handler on whose thread to execute the callbacks of the {@code callback}
+ * object. If null, then the application's main thread will be used.
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+ public void registerSoftApCallback(@NonNull SoftApCallback callback,
+ @Nullable Handler handler) {
+ if (callback == null) throw new IllegalArgumentException("callback cannot be null");
+ Log.v(TAG, "registerSoftApCallback: callback=" + callback + ", handler=" + handler);
+
+ Looper looper = (handler == null) ? mContext.getMainLooper() : handler.getLooper();
+ Binder binder = new Binder();
+ try {
+ mService.registerSoftApCallback(binder, new SoftApCallbackProxy(looper, callback),
+ callback.hashCode());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Allow callers to unregister a previously registered callback. After calling this method,
+ * applications will no longer receive soft AP events.
+ *
+ * @param callback Callback to unregister for soft AP events
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+ public void unregisterSoftApCallback(@NonNull SoftApCallback callback) {
+ if (callback == null) throw new IllegalArgumentException("callback cannot be null");
+ Log.v(TAG, "unregisterSoftApCallback: callback=" + callback);
+
+ try {
+ mService.unregisterSoftApCallback(callback.hashCode());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* LocalOnlyHotspotReservation that contains the {@link WifiConfiguration} for the active
* LocalOnlyHotspot request.
* <p>
@@ -2948,7 +3052,7 @@
public void disableEphemeralNetwork(String SSID) {
if (SSID == null) throw new IllegalArgumentException("SSID cannot be null");
try {
- mService.disableEphemeralNetwork(SSID);
+ mService.disableEphemeralNetwork(SSID, mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -2961,6 +3065,7 @@
* @param listener for callbacks on success or failure. Can be null.
* @throws IllegalStateException if the WifiManager instance needs to be
* initialized again
+ * @deprecated This API is deprecated
*/
public void startWps(WpsInfo config, WpsCallback listener) {
if (config == null) throw new IllegalArgumentException("config cannot be null");
@@ -2973,6 +3078,7 @@
* @param listener for callbacks on success or failure. Can be null.
* @throws IllegalStateException if the WifiManager instance needs to be
* initialized again
+ * @deprecated This API is deprecated
*/
public void cancelWps(WpsCallback listener) {
getChannel().sendMessage(CANCEL_WPS, 0, putListener(listener));
@@ -2987,7 +3093,7 @@
*/
public Messenger getWifiServiceMessenger() {
try {
- return mService.getWifiServiceMessenger();
+ return mService.getWifiServiceMessenger(mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -3123,7 +3229,7 @@
public void setWorkSource(WorkSource ws) {
synchronized (mBinder) {
- if (ws != null && ws.size() == 0) {
+ if (ws != null && ws.isEmpty()) {
ws = null;
}
boolean changed = true;
@@ -3135,7 +3241,7 @@
changed = mWorkSource != null;
mWorkSource = new WorkSource(ws);
} else {
- changed = mWorkSource.diff(ws);
+ changed = !mWorkSource.equals(ws);
if (changed) {
mWorkSource.set(ws);
}
@@ -3487,33 +3593,13 @@
}
/**
- * Deprecated
- * Does nothing
- * @hide
- * @deprecated
- */
- public void setAllowScansWithTraffic(int enabled) {
- return;
- }
-
- /**
- * Deprecated
- * returns value for 'disabled'
- * @hide
- * @deprecated
- */
- public int getAllowScansWithTraffic() {
- return 0;
- }
-
- /**
* Resets all wifi manager settings back to factory defaults.
*
* @hide
*/
public void factoryReset() {
try {
- mService.factoryReset();
+ mService.factoryReset(mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/android/net/wifi/aware/DiscoverySessionCallback.java b/android/net/wifi/aware/DiscoverySessionCallback.java
index aa2c268..2052f15 100644
--- a/android/net/wifi/aware/DiscoverySessionCallback.java
+++ b/android/net/wifi/aware/DiscoverySessionCallback.java
@@ -131,7 +131,6 @@
* {@link SubscribeConfig#SUBSCRIBE_TYPE_ACTIVE} discovery sessions this
* is the subscriber's match filter.
* @param distanceMm The measured distance to the Publisher in mm.
- * @hide
*/
public void onServiceDiscoveredWithinRange(PeerHandle peerHandle,
byte[] serviceSpecificInfo, List<byte[]> matchFilter, int distanceMm) {
diff --git a/android/net/wifi/aware/PublishConfig.java b/android/net/wifi/aware/PublishConfig.java
index 7a5049d..7a0250b 100644
--- a/android/net/wifi/aware/PublishConfig.java
+++ b/android/net/wifi/aware/PublishConfig.java
@@ -376,8 +376,6 @@
*
* @return The builder to facilitate chaining
* {@code builder.setXXX(..).setXXX(..)}.
- *
- * @hide
*/
public Builder setRangingEnabled(boolean enable) {
mEnableRanging = enable;
diff --git a/android/net/wifi/aware/SubscribeConfig.java b/android/net/wifi/aware/SubscribeConfig.java
index 91f8e52..2eab76a 100644
--- a/android/net/wifi/aware/SubscribeConfig.java
+++ b/android/net/wifi/aware/SubscribeConfig.java
@@ -435,8 +435,6 @@
*
* @return The builder to facilitate chaining
* {@code builder.setXXX(..).setXXX(..)}.
- *
- * @hide
*/
public Builder setMinDistanceMm(int minDistanceMm) {
mMinDistanceMm = minDistanceMm;
@@ -466,8 +464,6 @@
*
* @return The builder to facilitate chaining
* {@code builder.setXXX(..).setXXX(..)}.
- *
- * @hide
*/
public Builder setMaxDistanceMm(int maxDistanceMm) {
mMaxDistanceMm = maxDistanceMm;
diff --git a/android/net/wifi/aware/WifiAwareManager.java b/android/net/wifi/aware/WifiAwareManager.java
index d57d152..2f0c316 100644
--- a/android/net/wifi/aware/WifiAwareManager.java
+++ b/android/net/wifi/aware/WifiAwareManager.java
@@ -269,6 +269,10 @@
+ identityChangedListener);
}
+ if (attachCallback == null) {
+ throw new IllegalArgumentException("Null callback provided");
+ }
+
synchronized (mLock) {
Looper looper = (handler == null) ? Looper.getMainLooper() : handler.getLooper();
@@ -300,6 +304,10 @@
DiscoverySessionCallback callback) {
if (VDBG) Log.v(TAG, "publish(): clientId=" + clientId + ", config=" + publishConfig);
+ if (callback == null) {
+ throw new IllegalArgumentException("Null callback provided");
+ }
+
try {
mService.publish(mContext.getOpPackageName(), clientId, publishConfig,
new WifiAwareDiscoverySessionCallbackProxy(this, looper, true, callback,
@@ -333,6 +341,10 @@
}
}
+ if (callback == null) {
+ throw new IllegalArgumentException("Null callback provided");
+ }
+
try {
mService.subscribe(mContext.getOpPackageName(), clientId, subscribeConfig,
new WifiAwareDiscoverySessionCallbackProxy(this, looper, false, callback,
diff --git a/android/net/wifi/rtt/LocationCivic.java b/android/net/wifi/rtt/LocationCivic.java
new file mode 100644
index 0000000..610edb6
--- /dev/null
+++ b/android/net/wifi/rtt/LocationCivic.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2018 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.net.wifi.rtt;
+
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Location Civic Report (LCR).
+ * <p>
+ * The information matches the IEEE 802.11-2016 LCR report.
+ * <p>
+ * Note: depending on the mechanism by which this information is returned (i.e. the API which
+ * returns an instance of this class) it is possibly Self Reported (by the peer). In such a case
+ * the information is NOT validated - use with caution. Consider validating it with other sources
+ * of information before using it.
+ */
+public final class LocationCivic implements Parcelable {
+ private final byte[] mData;
+
+ /**
+ * Parse the raw LCR information element (byte array) and extract the LocationCivic structure.
+ *
+ * Note: any parsing errors or invalid/unexpected errors will result in a null being returned.
+ *
+ * @hide
+ */
+ @Nullable
+ public static LocationCivic parseInformationElement(byte id, byte[] data) {
+ // TODO
+ return null;
+ }
+
+ /** @hide */
+ public LocationCivic(byte[] data) {
+ mData = data;
+ }
+
+ /**
+ * Return the Location Civic data reported by the peer.
+ *
+ * @return An arbitrary location information.
+ */
+ public byte[] getData() {
+ return mData;
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByteArray(mData);
+ }
+
+ public static final Parcelable.Creator<LocationCivic> CREATOR =
+ new Parcelable.Creator<LocationCivic>() {
+ @Override
+ public LocationCivic[] newArray(int size) {
+ return new LocationCivic[size];
+ }
+
+ @Override
+ public LocationCivic createFromParcel(Parcel in) {
+ byte[] data = in.createByteArray();
+
+ return new LocationCivic(data);
+ }
+ };
+
+ /** @hide */
+ @Override
+ public String toString() {
+ return new StringBuilder("LCR: data=").append(Arrays.toString(mData)).toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof LocationCivic)) {
+ return false;
+ }
+
+ LocationCivic lhs = (LocationCivic) o;
+
+ return Arrays.equals(mData, lhs.mData);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mData);
+ }
+}
diff --git a/android/net/wifi/rtt/LocationConfigurationInformation.java b/android/net/wifi/rtt/LocationConfigurationInformation.java
new file mode 100644
index 0000000..8aba56a
--- /dev/null
+++ b/android/net/wifi/rtt/LocationConfigurationInformation.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2018 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.net.wifi.rtt;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * The Device Location Configuration Information (LCI) specifies the location information of a peer
+ * device (e.g. an Access Point).
+ * <p>
+ * The information matches the IEEE 802.11-2016 LCI report (Location configuration information
+ * report).
+ * <p>
+ * Note: depending on the mechanism by which this information is returned (i.e. the API which
+ * returns an instance of this class) it is possibly Self Reported (by the peer). In such a case
+ * the information is NOT validated - use with caution. Consider validating it with other sources
+ * of information before using it.
+ */
+public final class LocationConfigurationInformation implements Parcelable {
+ /** @hide */
+ @IntDef({
+ ALTITUDE_UNKNOWN, ALTITUDE_IN_METERS, ALTITUDE_IN_FLOORS })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AltitudeTypes {
+ }
+
+ /**
+ * Define an Altitude Type returned by {@link #getAltitudeType()}. Indicates that the location
+ * does not specify an altitude or altitude uncertainty. The corresponding methods,
+ * {@link #getAltitude()} and {@link #getAltitudeUncertainty()} are not valid and will throw
+ * an exception.
+ */
+ public static final int ALTITUDE_UNKNOWN = 0;
+
+ /**
+ * Define an Altitude Type returned by {@link #getAltitudeType()}. Indicates that the location
+ * specifies the altitude and altitude uncertainty in meters. The corresponding methods,
+ * {@link #getAltitude()} and {@link #getAltitudeUncertainty()} return a valid value in meters.
+ */
+ public static final int ALTITUDE_IN_METERS = 1;
+
+ /**
+ * Define an Altitude Type returned by {@link #getAltitudeType()}. Indicates that the
+ * location specifies the altitude in floors, and does not specify an altitude uncertainty.
+ * The {@link #getAltitude()} method returns valid value in floors, and the
+ * {@link #getAltitudeUncertainty()} method is not valid and will throw an exception.
+ */
+ public static final int ALTITUDE_IN_FLOORS = 2;
+
+ private final double mLatitude;
+ private final double mLatitudeUncertainty;
+ private final double mLongitude;
+ private final double mLongitudeUncertainty;
+ private final int mAltitudeType;
+ private final double mAltitude;
+ private final double mAltitudeUncertainty;
+
+ /**
+ * Parse the raw LCI information element (byte array) and extract the
+ * LocationConfigurationInformation structure.
+ *
+ * Note: any parsing errors or invalid/unexpected errors will result in a null being returned.
+ *
+ * @hide
+ */
+ @Nullable
+ public static LocationConfigurationInformation parseInformationElement(byte id, byte[] data) {
+ // TODO
+ return null;
+ }
+
+ /** @hide */
+ public LocationConfigurationInformation(double latitude, double latitudeUncertainty,
+ double longitude, double longitudeUncertainty, @AltitudeTypes int altitudeType,
+ double altitude, double altitudeUncertainty) {
+ mLatitude = latitude;
+ mLatitudeUncertainty = latitudeUncertainty;
+ mLongitude = longitude;
+ mLongitudeUncertainty = longitudeUncertainty;
+ mAltitudeType = altitudeType;
+ mAltitude = altitude;
+ mAltitudeUncertainty = altitudeUncertainty;
+ }
+
+ /**
+ * Get latitude in degrees. Values are per WGS 84 reference system. Valid values are between
+ * -90 and 90.
+ *
+ * @return Latitude in degrees.
+ */
+ public double getLatitude() {
+ return mLatitude;
+ }
+
+ /**
+ * Get the uncertainty of the latitude {@link #getLatitude()} in degrees. A value of 0 indicates
+ * an unknown uncertainty.
+ *
+ * @return Uncertainty of the latitude in degrees.
+ */
+ public double getLatitudeUncertainty() {
+ return mLatitudeUncertainty;
+ }
+
+ /**
+ * Get longitude in degrees. Values are per WGS 84 reference system. Valid values are between
+ * -180 and 180.
+ *
+ * @return Longitude in degrees.
+ */
+ public double getLongitude() {
+ return mLongitude;
+ }
+
+ /**
+ * Get the uncertainty of the longitude {@link #getLongitude()} ()} in degrees. A value of 0
+ * indicates an unknown uncertainty.
+ *
+ * @return Uncertainty of the longitude in degrees.
+ */
+ public double getLongitudeUncertainty() {
+ return mLongitudeUncertainty;
+ }
+
+ /**
+ * Specifies the type of the altitude measurement returned by {@link #getAltitude()} and
+ * {@link #getAltitudeUncertainty()}. The possible values are:
+ * <li>{@link #ALTITUDE_UNKNOWN}: The altitude and altitude uncertainty are not provided.
+ * <li>{@link #ALTITUDE_IN_METERS}: The altitude and altitude uncertainty are provided in
+ * meters. Values are per WGS 84 reference system.
+ * <li>{@link #ALTITUDE_IN_FLOORS}: The altitude is provided in floors, the altitude uncertainty
+ * is not provided.
+ *
+ * @return The type of the altitude and altitude uncertainty.
+ */
+ public @AltitudeTypes int getAltitudeType() {
+ return mAltitudeType;
+ }
+
+ /**
+ * The altitude is interpreted according to the {@link #getAltitudeType()}. The possible values
+ * are:
+ * <li>{@link #ALTITUDE_UNKNOWN}: The altitude is not provided - this method will throw an
+ * exception.
+ * <li>{@link #ALTITUDE_IN_METERS}: The altitude is provided in meters. Values are per WGS 84
+ * reference system.
+ * <li>{@link #ALTITUDE_IN_FLOORS}: The altitude is provided in floors.
+ *
+ * @return Altitude value whose meaning is specified by {@link #getAltitudeType()}.
+ */
+ public double getAltitude() {
+ if (mAltitudeType == ALTITUDE_UNKNOWN) {
+ throw new IllegalStateException(
+ "getAltitude(): invoked on an invalid type: getAltitudeType()==UNKNOWN");
+ }
+ return mAltitude;
+ }
+
+ /**
+ * Only valid if the the {@link #getAltitudeType()} is equal to {@link #ALTITUDE_IN_METERS} -
+ * otherwise this method will throw an exception.
+ * <p>
+ * Get the uncertainty of the altitude {@link #getAltitude()} in meters. A value of 0
+ * indicates an unknown uncertainty.
+ *
+ * @return Uncertainty of the altitude in meters.
+ */
+ public double getAltitudeUncertainty() {
+ if (mAltitudeType != ALTITUDE_IN_METERS) {
+ throw new IllegalStateException(
+ "getAltitude(): invoked on an invalid type: getAltitudeType()!=IN_METERS");
+ }
+ return mAltitudeUncertainty;
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeDouble(mLatitude);
+ dest.writeDouble(mLatitudeUncertainty);
+ dest.writeDouble(mLongitude);
+ dest.writeDouble(mLongitudeUncertainty);
+ dest.writeInt(mAltitudeType);
+ dest.writeDouble(mAltitude);
+ dest.writeDouble(mAltitudeUncertainty);
+ }
+
+ public static final Creator<LocationConfigurationInformation> CREATOR =
+ new Creator<LocationConfigurationInformation>() {
+ @Override
+ public LocationConfigurationInformation[] newArray(int size) {
+ return new LocationConfigurationInformation[size];
+ }
+
+ @Override
+ public LocationConfigurationInformation createFromParcel(Parcel in) {
+ double latitude = in.readDouble();
+ double latitudeUnc = in.readDouble();
+ double longitude = in.readDouble();
+ double longitudeUnc = in.readDouble();
+ int altitudeType = in.readInt();
+ double altitude = in.readDouble();
+ double altitudeUnc = in.readDouble();
+
+ return new LocationConfigurationInformation(latitude, latitudeUnc, longitude,
+ longitudeUnc, altitudeType, altitude, altitudeUnc);
+ }
+ };
+
+ /** @hide */
+ @Override
+ public String toString() {
+ return new StringBuilder("LCI: latitude=").append(mLatitude).append(
+ ", latitudeUncertainty=").append(mLatitudeUncertainty).append(
+ ", longitude=").append(mLongitude).append(", longitudeUncertainty=").append(
+ mLongitudeUncertainty).append(", altitudeType=").append(mAltitudeType).append(
+ ", altitude=").append(mAltitude).append(", altitudeUncertainty=").append(
+ mAltitudeUncertainty).toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof LocationConfigurationInformation)) {
+ return false;
+ }
+
+ LocationConfigurationInformation lhs = (LocationConfigurationInformation) o;
+
+ return mLatitude == lhs.mLatitude && mLatitudeUncertainty == lhs.mLatitudeUncertainty
+ && mLongitude == lhs.mLongitude
+ && mLongitudeUncertainty == lhs.mLongitudeUncertainty
+ && mAltitudeType == lhs.mAltitudeType && mAltitude == lhs.mAltitude
+ && mAltitudeUncertainty == lhs.mAltitudeUncertainty;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mLatitude, mLatitudeUncertainty, mLongitude, mLongitudeUncertainty,
+ mAltitudeType, mAltitude, mAltitudeUncertainty);
+ }
+}
diff --git a/android/net/wifi/rtt/RangingRequest.java b/android/net/wifi/rtt/RangingRequest.java
index b4e3097..32f21b9 100644
--- a/android/net/wifi/rtt/RangingRequest.java
+++ b/android/net/wifi/rtt/RangingRequest.java
@@ -17,6 +17,7 @@
package android.net.wifi.rtt;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.net.MacAddress;
import android.net.wifi.ScanResult;
import android.net.wifi.aware.AttachCallback;
@@ -41,8 +42,6 @@
* The ranging request is a batch request - specifying a set of devices (specified using
* {@link RangingRequest.Builder#addAccessPoint(ScanResult)} and
* {@link RangingRequest.Builder#addAccessPoints(List)}).
- *
- * @hide RTT_API
*/
public final class RangingRequest implements Parcelable {
private static final int MAX_PEERS = 10;
@@ -198,7 +197,7 @@
return addResponder(ResponderConfig.fromWifiAwarePeerHandleWithDefaults(peerHandle));
}
- /*
+ /**
* Add the Responder device specified by the {@link ResponderConfig} to the list of devices
* with which to measure range. The total number of peers added to the request cannot exceed
* the limit specified by {@link #getMaxPeers()}.
@@ -206,8 +205,9 @@
* @param responder Information on the RTT Responder.
* @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
*
- * @hide (SystemApi)
+ * @hide
*/
+ @SystemApi
public Builder addResponder(@NonNull ResponderConfig responder) {
if (responder == null) {
throw new IllegalArgumentException("Null Responder!");
diff --git a/android/net/wifi/rtt/RangingResult.java b/android/net/wifi/rtt/RangingResult.java
index a380fae..201833b 100644
--- a/android/net/wifi/rtt/RangingResult.java
+++ b/android/net/wifi/rtt/RangingResult.java
@@ -18,6 +18,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.net.MacAddress;
import android.net.wifi.aware.PeerHandle;
import android.os.Handler;
@@ -36,8 +37,6 @@
* <p>
* A ranging result is the distance measurement result for a single device specified in the
* {@link RangingRequest}.
- *
- * @hide RTT_API
*/
public final class RangingResult implements Parcelable {
private static final String TAG = "RangingResult";
@@ -66,29 +65,37 @@
private final int mDistanceMm;
private final int mDistanceStdDevMm;
private final int mRssi;
+ private final LocationConfigurationInformation mLci;
+ private final LocationCivic mLcr;
private final long mTimestamp;
/** @hide */
public RangingResult(@RangeResultStatus int status, @NonNull MacAddress mac, int distanceMm,
- int distanceStdDevMm, int rssi, long timestamp) {
+ int distanceStdDevMm, int rssi, LocationConfigurationInformation lci, LocationCivic lcr,
+ long timestamp) {
mStatus = status;
mMac = mac;
mPeerHandle = null;
mDistanceMm = distanceMm;
mDistanceStdDevMm = distanceStdDevMm;
mRssi = rssi;
+ mLci = lci;
+ mLcr = lcr;
mTimestamp = timestamp;
}
/** @hide */
public RangingResult(@RangeResultStatus int status, PeerHandle peerHandle, int distanceMm,
- int distanceStdDevMm, int rssi, long timestamp) {
+ int distanceStdDevMm, int rssi, LocationConfigurationInformation lci, LocationCivic lcr,
+ long timestamp) {
mStatus = status;
mMac = null;
mPeerHandle = peerHandle;
mDistanceMm = distanceMm;
mDistanceStdDevMm = distanceStdDevMm;
mRssi = rssi;
+ mLci = lci;
+ mLcr = lcr;
mTimestamp = timestamp;
}
@@ -108,6 +115,7 @@
* Will return a {@code null} for results corresponding to requests issued using a {@code
* PeerHandle}, i.e. using the {@link RangingRequest.Builder#addWifiAwarePeer(PeerHandle)} API.
*/
+ @Nullable
public MacAddress getMacAddress() {
return mMac;
}
@@ -119,7 +127,7 @@
* <p>
* Will return a {@code null} for results corresponding to requests issued using a MAC address.
*/
- public PeerHandle getPeerHandle() {
+ @Nullable public PeerHandle getPeerHandle() {
return mPeerHandle;
}
@@ -169,6 +177,38 @@
}
/**
+ * @return The Location Configuration Information (LCI) as self-reported by the peer.
+ * <p>
+ * Note: the information is NOT validated - use with caution. Consider validating it with
+ * other sources of information before using it.
+ */
+ @Nullable
+ public LocationConfigurationInformation getReportedLocationConfigurationInformation() {
+ if (mStatus != STATUS_SUCCESS) {
+ throw new IllegalStateException(
+ "getReportedLocationConfigurationInformation(): invoked on an invalid result: "
+ + "getStatus()=" + mStatus);
+ }
+ return mLci;
+ }
+
+ /**
+ * @return The Location Civic report (LCR) as self-reported by the peer.
+ * <p>
+ * Note: the information is NOT validated - use with caution. Consider validating it with
+ * other sources of information before using it.
+ */
+ @Nullable
+ public LocationCivic getReportedLocationCivic() {
+ if (mStatus != STATUS_SUCCESS) {
+ throw new IllegalStateException(
+ "getReportedLocationCivic(): invoked on an invalid result: getStatus()="
+ + mStatus);
+ }
+ return mLcr;
+ }
+
+ /**
* @return The timestamp, in us since boot, at which the ranging operation was performed.
* <p>
* Only valid if {@link #getStatus()} returns {@link #STATUS_SUCCESS}, otherwise will throw an
@@ -182,13 +222,11 @@
return mTimestamp;
}
- /** @hide */
@Override
public int describeContents() {
return 0;
}
- /** @hide */
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mStatus);
@@ -207,10 +245,21 @@
dest.writeInt(mDistanceMm);
dest.writeInt(mDistanceStdDevMm);
dest.writeInt(mRssi);
+ if (mLci == null) {
+ dest.writeBoolean(false);
+ } else {
+ dest.writeBoolean(true);
+ mLci.writeToParcel(dest, flags);
+ }
+ if (mLcr == null) {
+ dest.writeBoolean(false);
+ } else {
+ dest.writeBoolean(true);
+ mLcr.writeToParcel(dest, flags);
+ }
dest.writeLong(mTimestamp);
}
- /** @hide */
public static final Creator<RangingResult> CREATOR = new Creator<RangingResult>() {
@Override
public RangingResult[] newArray(int size) {
@@ -233,13 +282,23 @@
int distanceMm = in.readInt();
int distanceStdDevMm = in.readInt();
int rssi = in.readInt();
+ boolean lciPresent = in.readBoolean();
+ LocationConfigurationInformation lci = null;
+ if (lciPresent) {
+ lci = LocationConfigurationInformation.CREATOR.createFromParcel(in);
+ }
+ boolean lcrPresent = in.readBoolean();
+ LocationCivic lcr = null;
+ if (lcrPresent) {
+ lcr = LocationCivic.CREATOR.createFromParcel(in);
+ }
long timestamp = in.readLong();
if (peerHandlePresent) {
return new RangingResult(status, peerHandle, distanceMm, distanceStdDevMm, rssi,
- timestamp);
+ lci, lcr, timestamp);
} else {
return new RangingResult(status, mac, distanceMm, distanceStdDevMm, rssi,
- timestamp);
+ lci, lcr, timestamp);
}
}
};
@@ -251,8 +310,8 @@
mMac).append(", peerHandle=").append(
mPeerHandle == null ? "<null>" : mPeerHandle.peerId).append(", distanceMm=").append(
mDistanceMm).append(", distanceStdDevMm=").append(mDistanceStdDevMm).append(
- ", rssi=").append(mRssi).append(", timestamp=").append(mTimestamp).append(
- "]").toString();
+ ", rssi=").append(mRssi).append(", lci=").append(mLci).append(", lcr=").append(
+ mLcr).append(", timestamp=").append(mTimestamp).append("]").toString();
}
@Override
@@ -270,12 +329,13 @@
return mStatus == lhs.mStatus && Objects.equals(mMac, lhs.mMac) && Objects.equals(
mPeerHandle, lhs.mPeerHandle) && mDistanceMm == lhs.mDistanceMm
&& mDistanceStdDevMm == lhs.mDistanceStdDevMm && mRssi == lhs.mRssi
+ && Objects.equals(mLci, lhs.mLci) && Objects.equals(mLcr, lhs.mLcr)
&& mTimestamp == lhs.mTimestamp;
}
@Override
public int hashCode() {
return Objects.hash(mStatus, mMac, mPeerHandle, mDistanceMm, mDistanceStdDevMm, mRssi,
- mTimestamp);
+ mLci, mLcr, mTimestamp);
}
}
diff --git a/android/net/wifi/rtt/RangingResultCallback.java b/android/net/wifi/rtt/RangingResultCallback.java
index c8aea3c..9639dc8 100644
--- a/android/net/wifi/rtt/RangingResultCallback.java
+++ b/android/net/wifi/rtt/RangingResultCallback.java
@@ -17,6 +17,7 @@
package android.net.wifi.rtt;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.os.Handler;
import java.lang.annotation.Retention;
@@ -31,8 +32,6 @@
* peers then the {@link #onRangingResults(List)} will be called with the set of results (@link
* {@link RangingResult}, each of which has its own success/failure code
* {@link RangingResult#getStatus()}.
- *
- * @hide RTT_API
*/
public abstract class RangingResultCallback {
/** @hide */
@@ -68,5 +67,5 @@
*
* @param results List of range measurements, one per requested device.
*/
- public abstract void onRangingResults(List<RangingResult> results);
+ public abstract void onRangingResults(@NonNull List<RangingResult> results);
}
diff --git a/android/net/wifi/rtt/ResponderConfig.java b/android/net/wifi/rtt/ResponderConfig.java
index c3e1007..fb723c5 100644
--- a/android/net/wifi/rtt/ResponderConfig.java
+++ b/android/net/wifi/rtt/ResponderConfig.java
@@ -18,6 +18,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.net.MacAddress;
import android.net.wifi.ScanResult;
import android.net.wifi.aware.PeerHandle;
@@ -35,8 +36,9 @@
* A Responder configuration may be constructed from a {@link ScanResult} or manually (with the
* data obtained out-of-band from a peer).
*
- * @hide (@SystemApi)
+ * @hide
*/
+@SystemApi
public final class ResponderConfig implements Parcelable {
private static final int AWARE_BAND_2_DISCOVERY_CHANNEL = 2437;
@@ -290,7 +292,7 @@
MacAddress macAddress = MacAddress.fromString(scanResult.BSSID);
int responderType = RESPONDER_AP;
boolean supports80211mc = scanResult.is80211mcResponder();
- int channelWidth = translcateScanResultChannelWidth(scanResult.channelWidth);
+ int channelWidth = translateScanResultChannelWidth(scanResult.channelWidth);
int frequency = scanResult.frequency;
int centerFreq0 = scanResult.centerFreq0;
int centerFreq1 = scanResult.centerFreq1;
@@ -454,7 +456,7 @@
}
/** @hide */
- static int translcateScanResultChannelWidth(int scanResultChannelWidth) {
+ static int translateScanResultChannelWidth(int scanResultChannelWidth) {
switch (scanResultChannelWidth) {
case ScanResult.CHANNEL_WIDTH_20MHZ:
return CHANNEL_WIDTH_20MHZ;
@@ -468,7 +470,7 @@
return CHANNEL_WIDTH_80MHZ_PLUS_MHZ;
default:
throw new IllegalArgumentException(
- "translcateScanResultChannelWidth: bad " + scanResultChannelWidth);
+ "translateScanResultChannelWidth: bad " + scanResultChannelWidth);
}
}
}
diff --git a/android/net/wifi/rtt/WifiRttManager.java b/android/net/wifi/rtt/WifiRttManager.java
index b4c690f..ec6c46e 100644
--- a/android/net/wifi/rtt/WifiRttManager.java
+++ b/android/net/wifi/rtt/WifiRttManager.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2018 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.net.wifi.rtt;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
@@ -5,6 +21,7 @@
import static android.Manifest.permission.CHANGE_WIFI_STATE;
import static android.Manifest.permission.LOCATION_HARDWARE;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
@@ -38,8 +55,6 @@
* changes in RTT usability register for the {@link #ACTION_WIFI_RTT_STATE_CHANGED}
* broadcast. Note that this broadcast is not sticky - you should register for it and then
* check the above API to avoid a race condition.
- *
- * @hide RTT_API
*/
@SystemService(Context.WIFI_RTT_RANGING_SERVICE)
public class WifiRttManager {
@@ -71,6 +86,8 @@
* Returns the current status of RTT API: whether or not RTT is available. To track
* changes in the state of RTT API register for the
* {@link #ACTION_WIFI_RTT_STATE_CHANGED} broadcast.
+ * <p>Note: availability of RTT does not mean that the app can use the API. The app's
+ * permissions and platform Location Mode are validated at run-time.
*
* @return A boolean indicating whether the app can use the RTT API at this time (true) or
* not (false).
@@ -95,8 +112,8 @@
* will be used.
*/
@RequiresPermission(allOf = {ACCESS_COARSE_LOCATION, CHANGE_WIFI_STATE, ACCESS_WIFI_STATE})
- public void startRanging(RangingRequest request, RangingResultCallback callback,
- @Nullable Handler handler) {
+ public void startRanging(@NonNull RangingRequest request,
+ @NonNull RangingResultCallback callback, @Nullable Handler handler) {
startRanging(null, request, callback, handler);
}
@@ -112,17 +129,22 @@
* callback} object. If a null is provided then the application's main thread
* will be used.
*
- * @hide (@SystemApi)
+ * @hide
*/
+ @SystemApi
@RequiresPermission(allOf = {LOCATION_HARDWARE, ACCESS_COARSE_LOCATION, CHANGE_WIFI_STATE,
ACCESS_WIFI_STATE})
- public void startRanging(@Nullable WorkSource workSource, RangingRequest request,
- RangingResultCallback callback, @Nullable Handler handler) {
+ public void startRanging(@Nullable WorkSource workSource, @NonNull RangingRequest request,
+ @NonNull RangingResultCallback callback, @Nullable Handler handler) {
if (VDBG) {
Log.v(TAG, "startRanging: workSource=" + workSource + ", request=" + request
+ ", callback=" + callback + ", handler=" + handler);
}
+ if (callback == null) {
+ throw new IllegalArgumentException("Null callback provided");
+ }
+
Looper looper = (handler == null) ? Looper.getMainLooper() : handler.getLooper();
Binder binder = new Binder();
try {
@@ -139,10 +161,11 @@
*
* @param workSource The work-sources of the requesters.
*
- * @hide (@SystemApi)
+ * @hide
*/
+ @SystemApi
@RequiresPermission(allOf = {LOCATION_HARDWARE})
- public void cancelRanging(WorkSource workSource) {
+ public void cancelRanging(@Nullable WorkSource workSource) {
if (VDBG) {
Log.v(TAG, "cancelRanging: workSource=" + workSource);
}
diff --git a/android/os/BatteryStats.java b/android/os/BatteryStats.java
index 1e847c5..03a8dba 100644
--- a/android/os/BatteryStats.java
+++ b/android/os/BatteryStats.java
@@ -35,6 +35,7 @@
import android.view.Display;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.location.gnssmetrics.GnssMetrics;
import com.android.internal.os.BatterySipper;
import com.android.internal.os.BatteryStatsHelper;
@@ -180,6 +181,11 @@
public static final int FOREGROUND_SERVICE = 22;
/**
+ * A constant indicating an aggregate wifi multicast timer
+ */
+ public static final int WIFI_AGGREGATE_MULTICAST_ENABLED = 23;
+
+ /**
* Include all of the data in the stats, including previously saved data.
*/
public static final int STATS_SINCE_CHARGED = 0;
@@ -230,8 +236,11 @@
* New in version 29:
* - Process states re-ordered. TOP_SLEEPING now below BACKGROUND. HEAVY_WEIGHT introduced.
* - CPU times per UID process state
+ * New in version 30:
+ * - Uid.PROCESS_STATE_FOREGROUND_SERVICE only tracks
+ * ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE.
*/
- static final int CHECKIN_VERSION = 29;
+ static final int CHECKIN_VERSION = 30;
/**
* Old version, we hit 9 and ran out of room, need to remove.
@@ -327,6 +336,9 @@
private final StringBuilder mFormatBuilder = new StringBuilder(32);
private final Formatter mFormatter = new Formatter(mFormatBuilder);
+ private static final String CELLULAR_CONTROLLER_NAME = "Cellular";
+ private static final String WIFI_CONTROLLER_NAME = "WiFi";
+
/**
* Indicates times spent by the uid at each cpu frequency in all process states.
*
@@ -404,6 +416,13 @@
/**
* @return a non-null {@link LongCounter} representing time spent (milliseconds) in the
+ * scan state.
+ */
+ public abstract LongCounter getScanTimeCounter();
+
+
+ /**
+ * @return a non-null {@link LongCounter} representing time spent (milliseconds) in the
* receive state.
*/
public abstract LongCounter getRxTimeCounter();
@@ -520,8 +539,8 @@
return ActivityManager.PROCESS_STATE_NONEXISTENT;
} else if (procState == ActivityManager.PROCESS_STATE_TOP) {
return Uid.PROCESS_STATE_TOP;
- } else if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
- // Persistent and other foreground states go here.
+ } else if (procState == ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+ // State when app has put itself in the foreground.
return Uid.PROCESS_STATE_FOREGROUND_SERVICE;
} else if (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) {
// Persistent and other foreground states go here.
@@ -675,6 +694,14 @@
public abstract long[] getCpuFreqTimes(int which);
public abstract long[] getScreenOffCpuFreqTimes(int which);
+ /**
+ * Returns cpu active time of an uid.
+ */
+ public abstract long getCpuActiveTime();
+ /**
+ * Returns cpu times of an uid on each cluster
+ */
+ public abstract long[] getCpuClusterTimes();
/**
* Returns cpu times of an uid at a particular process state.
@@ -689,17 +716,17 @@
// total time a uid has had any processes running at all.
/**
- * Time this uid has any processes in the top state (or above such as persistent).
+ * Time this uid has any processes in the top state.
*/
public static final int PROCESS_STATE_TOP = 0;
/**
- * Time this uid has any process with a started out bound foreground service, but
+ * Time this uid has any process with a started foreground service, but
* none in the "top" state.
*/
public static final int PROCESS_STATE_FOREGROUND_SERVICE = 1;
/**
* Time this uid has any process in an active foreground state, but none in the
- * "top sleeping" or better state.
+ * "foreground service" or better state. Persistent and other foreground states go here.
*/
public static final int PROCESS_STATE_FOREGROUND = 2;
/**
@@ -1492,6 +1519,10 @@
public static final int STATE2_WIFI_SIGNAL_STRENGTH_SHIFT = 4;
public static final int STATE2_WIFI_SIGNAL_STRENGTH_MASK =
0x7 << STATE2_WIFI_SIGNAL_STRENGTH_SHIFT;
+ // Values for NUM_GPS_SIGNAL_QUALITY_LEVELS
+ public static final int STATE2_GPS_SIGNAL_QUALITY_SHIFT = 7;
+ public static final int STATE2_GPS_SIGNAL_QUALITY_MASK =
+ 0x1 << STATE2_GPS_SIGNAL_QUALITY_SHIFT;
public static final int STATE2_POWER_SAVE_FLAG = 1<<31;
public static final int STATE2_VIDEO_ON_FLAG = 1<<30;
@@ -2080,6 +2111,23 @@
*/
public abstract int getNumConnectivityChange(int which);
+
+ /**
+ * Returns the time in microseconds that the phone has been running with
+ * the given GPS signal quality level
+ *
+ * {@hide}
+ */
+ public abstract long getGpsSignalQualityTime(int strengthBin,
+ long elapsedRealtimeUs, int which);
+
+ /**
+ * Returns the GPS battery drain in mA-ms
+ *
+ * {@hide}
+ */
+ public abstract long getGpsBatteryDrainMaMs();
+
/**
* Returns the time in microseconds that the phone has been on while the device was
* running on battery.
@@ -2304,6 +2352,9 @@
WIFI_SUPPL_STATE_NAMES, WIFI_SUPPL_STATE_SHORT_NAMES),
new BitDescription(HistoryItem.STATE2_CAMERA_FLAG, "camera", "ca"),
new BitDescription(HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG, "ble_scan", "bles"),
+ new BitDescription(HistoryItem.STATE2_GPS_SIGNAL_QUALITY_MASK,
+ HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT, "gps_signal_quality", "Gss",
+ new String[] { "poor", "good"}, new String[] { "poor", "good"}),
};
public static final String[] HISTORY_EVENT_NAMES = new String[] {
@@ -2334,6 +2385,22 @@
};
/**
+ * Returns total time for WiFi Multicast Wakelock timer.
+ * Note that this may be different from the sum of per uid timer values.
+ *
+ * {@hide}
+ */
+ public abstract long getWifiMulticastWakelockTime(long elapsedRealtimeUs, int which);
+
+ /**
+ * Returns total time for WiFi Multicast Wakelock timer
+ * Note that this may be different from the sum of per uid timer values.
+ *
+ * {@hide}
+ */
+ public abstract int getWifiMulticastWakelockCount(int which);
+
+ /**
* Returns the time in microseconds that wifi has been on while the device was
* running on battery.
*
@@ -2342,6 +2409,14 @@
public abstract long getWifiOnTime(long elapsedRealtimeUs, int which);
/**
+ * Returns the time in microseconds that wifi has been active while the device was
+ * running on battery.
+ *
+ * {@hide}
+ */
+ public abstract long getWifiActiveTime(long elapsedRealtimeUs, int which);
+
+ /**
* Returns the time in microseconds that wifi has been on and the driver has
* been in the running state while the device was running on battery.
*
@@ -3288,6 +3363,20 @@
final long sleepTimeMs
= totalControllerActivityTimeMs - (idleTimeMs + rxTimeMs + totalTxTimeMs);
+ if (controllerName.equals(WIFI_CONTROLLER_NAME)) {
+ final long scanTimeMs = counter.getScanTimeCounter().getCountLocked(which);
+ sb.setLength(0);
+ sb.append(prefix);
+ sb.append(" ");
+ sb.append(controllerName);
+ sb.append(" Scan time: ");
+ formatTimeMs(sb, scanTimeMs);
+ sb.append("(");
+ sb.append(formatRatioLocked(scanTimeMs, totalControllerActivityTimeMs));
+ sb.append(")");
+ pw.println(sb.toString());
+ }
+
sb.setLength(0);
sb.append(prefix);
sb.append(" ");
@@ -3329,7 +3418,7 @@
String [] powerLevel;
switch(controllerName) {
- case "Cellular":
+ case CELLULAR_CONTROLLER_NAME:
powerLevel = new String[] {
" less than 0dBm: ",
" 0dBm to 8dBm: ",
@@ -3442,16 +3531,13 @@
screenDozeTime / 1000);
- // Calculate both wakelock and wifi multicast wakelock times across all uids.
+ // Calculate wakelock times across all uids.
long fullWakeLockTimeTotal = 0;
long partialWakeLockTimeTotal = 0;
- long multicastWakeLockTimeTotalMicros = 0;
- int multicastWakeLockCountTotal = 0;
for (int iu = 0; iu < NU; iu++) {
final Uid u = uidStats.valueAt(iu);
- // First calculating the wakelock stats
final ArrayMap<String, ? extends BatteryStats.Uid.Wakelock> wakelocks
= u.getWakelockStats();
for (int iw=wakelocks.size()-1; iw>=0; iw--) {
@@ -3469,13 +3555,6 @@
rawRealtime, which);
}
}
-
- // Now calculating the wifi multicast wakelock stats
- final Timer mcTimer = u.getMulticastWakelockStats();
- if (mcTimer != null) {
- multicastWakeLockTimeTotalMicros += mcTimer.getTotalTimeLocked(rawRealtime, which);
- multicastWakeLockCountTotal += mcTimer.getCountLocked(which);
- }
}
// Dump network stats
@@ -3592,6 +3671,9 @@
dumpLine(pw, 0 /* uid */, category, WIFI_SIGNAL_STRENGTH_COUNT_DATA, args);
// Dump Multicast total stats
+ final long multicastWakeLockTimeTotalMicros =
+ getWifiMulticastWakelockTime(rawRealtime, which);
+ final int multicastWakeLockCountTotal = getWifiMulticastWakelockCount(which);
dumpLine(pw, 0 /* uid */, category, WIFI_MULTICAST_TOTAL_DATA,
multicastWakeLockTimeTotalMicros / 1000,
multicastWakeLockCountTotal);
@@ -4456,18 +4538,15 @@
pw.print(" Connectivity changes: "); pw.println(connChanges);
}
- // Calculate both wakelock and wifi multicast wakelock times across all uids.
+ // Calculate wakelock times across all uids.
long fullWakeLockTimeTotalMicros = 0;
long partialWakeLockTimeTotalMicros = 0;
- long multicastWakeLockTimeTotalMicros = 0;
- int multicastWakeLockCountTotal = 0;
final ArrayList<TimerEntry> timers = new ArrayList<>();
for (int iu = 0; iu < NU; iu++) {
final Uid u = uidStats.valueAt(iu);
- // First calculate wakelock statistics
final ArrayMap<String, ? extends BatteryStats.Uid.Wakelock> wakelocks
= u.getWakelockStats();
for (int iw=wakelocks.size()-1; iw>=0; iw--) {
@@ -4495,13 +4574,6 @@
}
}
}
-
- // Next calculate wifi multicast wakelock statistics
- final Timer mcTimer = u.getMulticastWakelockStats();
- if (mcTimer != null) {
- multicastWakeLockTimeTotalMicros += mcTimer.getTotalTimeLocked(rawRealtime, which);
- multicastWakeLockCountTotal += mcTimer.getCountLocked(which);
- }
}
final long mobileRxTotalBytes = getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, which);
@@ -4531,6 +4603,9 @@
pw.println(sb.toString());
}
+ final long multicastWakeLockTimeTotalMicros =
+ getWifiMulticastWakelockTime(rawRealtime, which);
+ final int multicastWakeLockCountTotal = getWifiMulticastWakelockCount(which);
if (multicastWakeLockTimeTotalMicros != 0) {
sb.setLength(0);
sb.append(prefix);
@@ -4631,7 +4706,7 @@
if (!didOne) sb.append(" (no activity)");
pw.println(sb.toString());
- printControllerActivity(pw, sb, prefix, "Cellular",
+ printControllerActivity(pw, sb, prefix, CELLULAR_CONTROLLER_NAME,
getModemControllerActivity(), which);
pw.print(prefix);
@@ -4640,6 +4715,16 @@
sb.append(" Wifi Statistics:");
pw.println(sb.toString());
+ pw.print(prefix);
+ sb.setLength(0);
+ sb.append(prefix);
+ sb.append(" Wifi kernel active time: ");
+ final long wifiActiveTime = getWifiActiveTime(rawRealtime, which);
+ formatTimeMs(sb, wifiActiveTime / 1000);
+ sb.append("("); sb.append(formatRatioLocked(wifiActiveTime, whichBatteryRealtime));
+ sb.append(")");
+ pw.println(sb.toString());
+
pw.print(" Wifi data received: "); pw.println(formatBytesLocked(wifiRxTotalBytes));
pw.print(" Wifi data sent: "); pw.println(formatBytesLocked(wifiTxTotalBytes));
pw.print(" Wifi packets received: "); pw.println(wifiRxTotalPackets);
@@ -4717,7 +4802,45 @@
if (!didOne) sb.append(" (no activity)");
pw.println(sb.toString());
- printControllerActivity(pw, sb, prefix, "WiFi", getWifiControllerActivity(), which);
+ printControllerActivity(pw, sb, prefix, WIFI_CONTROLLER_NAME,
+ getWifiControllerActivity(), which);
+
+ pw.print(prefix);
+ sb.setLength(0);
+ sb.append(prefix);
+ sb.append(" GPS Statistics:");
+ pw.println(sb.toString());
+
+ sb.setLength(0);
+ sb.append(prefix);
+ sb.append(" GPS signal quality (Top 4 Average CN0):");
+ final String[] gpsSignalQualityDescription = new String[]{
+ "poor (less than 20 dBHz): ",
+ "good (greater than 20 dBHz): "};
+ final int numGpsSignalQualityBins = Math.min(GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS,
+ gpsSignalQualityDescription.length);
+ for (int i=0; i<numGpsSignalQualityBins; i++) {
+ final long time = getGpsSignalQualityTime(i, rawRealtime, which);
+ sb.append("\n ");
+ sb.append(prefix);
+ sb.append(" ");
+ sb.append(gpsSignalQualityDescription[i]);
+ formatTimeMs(sb, time/1000);
+ sb.append("(");
+ sb.append(formatRatioLocked(time, whichBatteryRealtime));
+ sb.append(") ");
+ }
+ pw.println(sb.toString());
+
+ final long gpsBatteryDrainMaMs = getGpsBatteryDrainMaMs();
+ if (gpsBatteryDrainMaMs > 0) {
+ pw.print(prefix);
+ sb.setLength(0);
+ sb.append(prefix);
+ sb.append(" Battery Drain (mAh): ");
+ sb.append(Double.toString(((double) gpsBatteryDrainMaMs)/(3600 * 1000)));
+ pw.println(sb.toString());
+ }
pw.print(prefix);
sb.setLength(0);
@@ -5158,8 +5281,8 @@
pw.println(sb.toString());
}
- printControllerActivityIfInteresting(pw, sb, prefix + " ", "Modem",
- u.getModemControllerActivity(), which);
+ printControllerActivityIfInteresting(pw, sb, prefix + " ",
+ CELLULAR_CONTROLLER_NAME, u.getModemControllerActivity(), which);
if (wifiRxBytes > 0 || wifiTxBytes > 0 || wifiRxPackets > 0 || wifiTxPackets > 0) {
pw.print(prefix); pw.print(" Wi-Fi network: ");
@@ -5213,7 +5336,7 @@
pw.println(sb.toString());
}
- printControllerActivityIfInteresting(pw, sb, prefix + " ", "WiFi",
+ printControllerActivityIfInteresting(pw, sb, prefix + " ", WIFI_CONTROLLER_NAME,
u.getWifiControllerActivity(), which);
if (btRxBytes > 0 || btTxBytes > 0) {
@@ -7051,6 +7174,28 @@
}
}
}
+
+ for (int procState = 0; procState < Uid.NUM_PROCESS_STATE; ++procState) {
+ final long[] timesMs = u.getCpuFreqTimes(which, procState);
+ if (timesMs != null && timesMs.length == cpuFreqs.length) {
+ long[] screenOffTimesMs = u.getScreenOffCpuFreqTimes(which, procState);
+ if (screenOffTimesMs == null) {
+ screenOffTimesMs = new long[timesMs.length];
+ }
+ final long procToken = proto.start(UidProto.Cpu.BY_PROCESS_STATE);
+ proto.write(UidProto.Cpu.ByProcessState.PROCESS_STATE, procState);
+ for (int ic = 0; ic < timesMs.length; ++ic) {
+ long cToken = proto.start(UidProto.Cpu.ByProcessState.BY_FREQUENCY);
+ proto.write(UidProto.Cpu.ByFrequency.FREQUENCY_INDEX, ic + 1);
+ proto.write(UidProto.Cpu.ByFrequency.TOTAL_DURATION_MS,
+ timesMs[ic]);
+ proto.write(UidProto.Cpu.ByFrequency.SCREEN_OFF_DURATION_MS,
+ screenOffTimesMs[ic]);
+ proto.end(cToken);
+ }
+ proto.end(procToken);
+ }
+ }
proto.end(cpuToken);
// Flashlight (FLASHLIGHT_DATA)
@@ -7535,22 +7680,9 @@
proto.end(mToken);
// Wifi multicast wakelock total stats (WIFI_MULTICAST_WAKELOCK_TOTAL_DATA)
- // Calculate multicast wakelock stats across all uids.
- long multicastWakeLockTimeTotalUs = 0;
- int multicastWakeLockCountTotal = 0;
-
- for (int iu = 0; iu < uidStats.size(); iu++) {
- final Uid u = uidStats.valueAt(iu);
-
- final Timer mcTimer = u.getMulticastWakelockStats();
-
- if (mcTimer != null) {
- multicastWakeLockTimeTotalUs +=
- mcTimer.getTotalTimeLocked(rawRealtimeUs, which);
- multicastWakeLockCountTotal += mcTimer.getCountLocked(which);
- }
- }
-
+ final long multicastWakeLockTimeTotalUs =
+ getWifiMulticastWakelockTime(rawRealtimeUs, which);
+ final int multicastWakeLockCountTotal = getWifiMulticastWakelockCount(which);
final long wmctToken = proto.start(SystemProto.WIFI_MULTICAST_WAKELOCK_TOTAL);
proto.write(SystemProto.WifiMulticastWakelockTotal.DURATION_MS,
multicastWakeLockTimeTotalUs / 1000);
diff --git a/android/os/Binder.java b/android/os/Binder.java
index 33470f3..eb264d6 100644
--- a/android/os/Binder.java
+++ b/android/os/Binder.java
@@ -805,7 +805,7 @@
/**
* Return the total number of pairs in the map.
*/
- int size() {
+ private int size() {
int size = 0;
for (ArrayList<WeakReference<BinderProxy>> a : mMainIndexValues) {
if (a != null) {
@@ -816,6 +816,24 @@
}
/**
+ * Return the total number of pairs in the map containing values that have
+ * not been cleared. More expensive than the above size function.
+ */
+ private int unclearedSize() {
+ int size = 0;
+ for (ArrayList<WeakReference<BinderProxy>> a : mMainIndexValues) {
+ if (a != null) {
+ for (WeakReference<BinderProxy> ref : a) {
+ if (ref.get() != null) {
+ ++size;
+ }
+ }
+ }
+ }
+ return size;
+ }
+
+ /**
* Remove ith entry from the hash bucket indicated by hash.
*/
private void remove(int hash, int index) {
@@ -908,17 +926,31 @@
Log.v(Binder.TAG, "BinderProxy map growth! bucket size = " + size
+ " total = " + totalSize);
mWarnBucketSize += WARN_INCREMENT;
- if (Build.IS_DEBUGGABLE && totalSize > CRASH_AT_SIZE) {
- diagnosticCrash();
+ if (Build.IS_DEBUGGABLE && totalSize >= CRASH_AT_SIZE) {
+ // Use the number of uncleared entries to determine whether we should
+ // really report a histogram and crash. We don't want to fundamentally
+ // change behavior for a debuggable process, so we GC only if we are
+ // about to crash.
+ final int totalUnclearedSize = unclearedSize();
+ if (totalUnclearedSize >= CRASH_AT_SIZE) {
+ dumpProxyInterfaceCounts();
+ Runtime.getRuntime().gc();
+ throw new AssertionError("Binder ProxyMap has too many entries: "
+ + totalSize + " (total), " + totalUnclearedSize + " (uncleared), "
+ + unclearedSize() + " (uncleared after GC). BinderProxy leak?");
+ } else if (totalSize > 3 * totalUnclearedSize / 2) {
+ Log.v(Binder.TAG, "BinderProxy map has many cleared entries: "
+ + (totalSize - totalUnclearedSize) + " of " + totalSize
+ + " are cleared");
+ }
}
}
}
/**
- * Dump a histogram to the logcat, then throw an assertion error. Used to diagnose
- * abnormally large proxy maps.
+ * Dump a histogram to the logcat. Used to diagnose abnormally large proxy maps.
*/
- private void diagnosticCrash() {
+ private void dumpProxyInterfaceCounts() {
Map<String, Integer> counts = new HashMap<>();
for (ArrayList<WeakReference<BinderProxy>> a : mMainIndexValues) {
if (a != null) {
@@ -953,11 +985,6 @@
Log.v(Binder.TAG, " #" + (i + 1) + ": " + sorted[i].getKey() + " x"
+ sorted[i].getValue());
}
-
- // Now throw an assertion.
- final int totalSize = size();
- throw new AssertionError("Binder ProxyMap has too many entries: " + totalSize
- + ". BinderProxy leak?");
}
// Corresponding ArrayLists in the following two arrays always have the same size.
diff --git a/android/os/Bundle.java b/android/os/Bundle.java
index c58153a..7ae5a67 100644
--- a/android/os/Bundle.java
+++ b/android/os/Bundle.java
@@ -21,6 +21,7 @@
import android.util.Size;
import android.util.SizeF;
import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
@@ -1272,4 +1273,21 @@
}
return mMap.toString();
}
+
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ if (mParcelledData != null) {
+ if (isEmptyParcel()) {
+ proto.write(BundleProto.PARCELLED_DATA_SIZE, 0);
+ } else {
+ proto.write(BundleProto.PARCELLED_DATA_SIZE, mParcelledData.dataSize());
+ }
+ } else {
+ proto.write(BundleProto.MAP_DATA, mMap.toString());
+ }
+
+ proto.end(token);
+ }
}
diff --git a/android/os/ConfigUpdate.java b/android/os/ConfigUpdate.java
index 94a44ec..dda0ed8 100644
--- a/android/os/ConfigUpdate.java
+++ b/android/os/ConfigUpdate.java
@@ -82,6 +82,14 @@
public static final String ACTION_UPDATE_SMART_SELECTION
= "android.intent.action.UPDATE_SMART_SELECTION";
+ /**
+ * Update network watchlist config file.
+ * @hide
+ */
+ @SystemApi
+ public static final String ACTION_UPDATE_NETWORK_WATCHLIST
+ = "android.intent.action.UPDATE_NETWORK_WATCHLIST";
+
private ConfigUpdate() {
}
}
diff --git a/android/os/Debug.java b/android/os/Debug.java
index 848ab88..33e8c3e 100644
--- a/android/os/Debug.java
+++ b/android/os/Debug.java
@@ -2352,22 +2352,28 @@
}
/**
- * Attach a library as a jvmti agent to the current runtime.
+ * Attach a library as a jvmti agent to the current runtime, with the given classloader
+ * determining the library search path.
+ * <p>
+ * Note: agents may only be attached to debuggable apps. Otherwise, this function will
+ * throw a SecurityException.
*
- * @param library library containing the agent
- * @param options options passed to the agent
+ * @param library the library containing the agent.
+ * @param options the options passed to the agent.
+ * @param classLoader the classloader determining the library search path.
*
- * @throws IOException If the agent could not be attached
+ * @throws IOException if the agent could not be attached.
+ * @throws SecurityException if the app is not debuggable.
*/
- public static void attachJvmtiAgent(@NonNull String library, @Nullable String options)
- throws IOException {
+ public static void attachJvmtiAgent(@NonNull String library, @Nullable String options,
+ @Nullable ClassLoader classLoader) throws IOException {
Preconditions.checkNotNull(library);
Preconditions.checkArgument(!library.contains("="));
if (options == null) {
- VMDebug.attachAgent(library);
+ VMDebug.attachAgent(library, classLoader);
} else {
- VMDebug.attachAgent(library + "=" + options);
+ VMDebug.attachAgent(library + "=" + options, classLoader);
}
}
}
diff --git a/android/os/Environment.java b/android/os/Environment.java
index b1794a6..62731e8 100644
--- a/android/os/Environment.java
+++ b/android/os/Environment.java
@@ -292,6 +292,16 @@
}
/** {@hide} */
+ public static File getDataVendorCeDirectory(int userId) {
+ return buildPath(getDataDirectory(), "vendor_ce", String.valueOf(userId));
+ }
+
+ /** {@hide} */
+ public static File getDataVendorDeDirectory(int userId) {
+ return buildPath(getDataDirectory(), "vendor_de", String.valueOf(userId));
+ }
+
+ /** {@hide} */
public static File getProfileSnapshotPath(String packageName, String codePath) {
return buildPath(buildPath(getDataDirectory(), "misc", "profiles", "ref", packageName,
"primary.prof.snapshot"));
diff --git a/android/os/Handler.java b/android/os/Handler.java
index 3ca1005..fc88e90 100644
--- a/android/os/Handler.java
+++ b/android/os/Handler.java
@@ -202,7 +202,8 @@
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
- "Can't create handler inside thread that has not called Looper.prepare()");
+ "Can't create handler inside thread " + Thread.currentThread()
+ + " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
@@ -388,6 +389,8 @@
* The runnable will be run on the thread to which this handler is attached.
*
* @param r The Runnable that will be executed.
+ * @param token An instance which can be used to cancel {@code r} via
+ * {@link #removeCallbacksAndMessages}.
* @param uptimeMillis The absolute time at which the callback should run,
* using the {@link android.os.SystemClock#uptimeMillis} time-base.
*
@@ -430,6 +433,32 @@
}
/**
+ * Causes the Runnable r to be added to the message queue, to be run
+ * after the specified amount of time elapses.
+ * The runnable will be run on the thread to which this handler
+ * is attached.
+ * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b>
+ * Time spent in deep sleep will add an additional delay to execution.
+ *
+ * @param r The Runnable that will be executed.
+ * @param token An instance which can be used to cancel {@code r} via
+ * {@link #removeCallbacksAndMessages}.
+ * @param delayMillis The delay (in milliseconds) until the Runnable
+ * will be executed.
+ *
+ * @return Returns true if the Runnable was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting. Note that a
+ * result of true does not mean the Runnable will be processed --
+ * if the looper is quit before the delivery time of the message
+ * occurs then the message will be dropped.
+ */
+ public final boolean postDelayed(Runnable r, Object token, long delayMillis)
+ {
+ return sendMessageDelayed(getPostMessage(r, token), delayMillis);
+ }
+
+ /**
* Posts a message to an object that implements Runnable.
* Causes the Runnable r to executed on the next iteration through the
* message queue. The runnable will be run on the thread to which this
diff --git a/android/os/HidlSupport.java b/android/os/HidlSupport.java
index a080c8d..335bf9d 100644
--- a/android/os/HidlSupport.java
+++ b/android/os/HidlSupport.java
@@ -16,6 +16,8 @@
package android.os;
+import android.annotation.SystemApi;
+
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
@@ -25,6 +27,7 @@
import java.util.stream.IntStream;
/** @hide */
+@SystemApi
public class HidlSupport {
/**
* Similar to Objects.deepEquals, but also take care of lists.
@@ -36,7 +39,9 @@
* 2.3 Both are Lists, elements are checked recursively
* 2.4 (If both are collections other than lists or maps, throw an error)
* 2.5 lft.equals(rgt) returns true
+ * @hide
*/
+ @SystemApi
public static boolean deepEquals(Object lft, Object rgt) {
if (lft == rgt) {
return true;
@@ -86,8 +91,30 @@
}
/**
- * Similar to Arrays.deepHashCode, but also take care of lists.
+ * Class which can be used to fetch an object out of a lambda. Fetching an object
+ * out of a local scope with HIDL is a common operation (although usually it can
+ * and should be avoided).
+ *
+ * @param <E> Inner object type.
+ * @hide
*/
+ public static final class Mutable<E> {
+ public E value;
+
+ public Mutable() {
+ value = null;
+ }
+
+ public Mutable(E value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Similar to Arrays.deepHashCode, but also take care of lists.
+ * @hide
+ */
+ @SystemApi
public static int deepHashCode(Object o) {
if (o == null) {
return 0;
@@ -114,6 +141,7 @@
return o.hashCode();
}
+ /** @hide */
private static void throwErrorIfUnsupportedType(Object o) {
if (o instanceof Collection<?> && !(o instanceof List<?>)) {
throw new UnsupportedOperationException(
@@ -127,6 +155,7 @@
}
}
+ /** @hide */
private static int primitiveArrayHashCode(Object o) {
Class<?> elementType = o.getClass().getComponentType();
if (elementType == boolean.class) {
@@ -166,7 +195,9 @@
* - If both interfaces are stubs, asBinder() returns the object itself. By default,
* auto-generated IFoo.Stub does not override equals(), but an implementation can
* optionally override it, and {@code interfacesEqual} will use it here.
+ * @hide
*/
+ @SystemApi
public static boolean interfacesEqual(IHwInterface lft, Object rgt) {
if (lft == rgt) {
return true;
@@ -182,6 +213,10 @@
/**
* Return PID of process if sharable to clients.
+ * @hide
*/
public static native int getPidIfSharable();
+
+ /** @hide */
+ public HidlSupport() {}
}
diff --git a/android/os/HwBinder.java b/android/os/HwBinder.java
index 5e2a081..ecac002 100644
--- a/android/os/HwBinder.java
+++ b/android/os/HwBinder.java
@@ -16,16 +16,20 @@
package android.os;
+import android.annotation.SystemApi;
+
import libcore.util.NativeAllocationRegistry;
import java.util.NoSuchElementException;
/** @hide */
+@SystemApi
public abstract class HwBinder implements IHwBinder {
private static final String TAG = "HwBinder";
private static final NativeAllocationRegistry sNativeRegistry;
+ /** @hide */
public HwBinder() {
native_setup();
@@ -34,33 +38,55 @@
mNativeContext);
}
+ /** @hide */
@Override
public final native void transact(
int code, HwParcel request, HwParcel reply, int flags)
throws RemoteException;
+ /** @hide */
public abstract void onTransact(
int code, HwParcel request, HwParcel reply, int flags)
throws RemoteException;
+ /** @hide */
public native final void registerService(String serviceName)
throws RemoteException;
+ /** @hide */
public static final IHwBinder getService(
String iface,
String serviceName)
throws RemoteException, NoSuchElementException {
return getService(iface, serviceName, false /* retry */);
}
+ /** @hide */
public static native final IHwBinder getService(
String iface,
String serviceName,
boolean retry)
throws RemoteException, NoSuchElementException;
+ /**
+ * Configures how many threads the process-wide hwbinder threadpool
+ * has to process incoming requests.
+ *
+ * @hide
+ */
+ @SystemApi
public static native final void configureRpcThreadpool(
long maxThreads, boolean callerWillJoin);
+ /**
+ * Current thread will join hwbinder threadpool and process
+ * commands in the pool. Should be called after configuring
+ * a threadpool with callerWillJoin true and then registering
+ * the provided service if this thread doesn't need to do
+ * anything else.
+ *
+ * @hide
+ */
+ @SystemApi
public static native final void joinRpcThreadpool();
// Returns address of the "freeFunction".
@@ -83,6 +109,7 @@
/**
* Notifies listeners that a system property has changed
+ * @hide
*/
public static void reportSyspropChanged() {
native_report_sysprop_change();
diff --git a/android/os/HwBlob.java b/android/os/HwBlob.java
index 5e9b9ae..405651e 100644
--- a/android/os/HwBlob.java
+++ b/android/os/HwBlob.java
@@ -17,10 +17,17 @@
package android.os;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import libcore.util.NativeAllocationRegistry;
-/** @hide */
+/**
+ * Represents fixed sized allocation of marshalled data used. Helper methods
+ * allow for access to the unmarshalled data in a variety of ways.
+ *
+ * @hide
+ */
+@SystemApi
public class HwBlob {
private static final String TAG = "HwBlob";
@@ -34,48 +41,276 @@
mNativeContext);
}
+ /**
+ * @param offset offset to unmarshall a boolean from
+ * @return the unmarshalled boolean value
+ * @throws IndexOutOfBoundsException when offset is out of this HwBlob
+ */
public native final boolean getBool(long offset);
+ /**
+ * @param offset offset to unmarshall a byte from
+ * @return the unmarshalled byte value
+ * @throws IndexOutOfBoundsException when offset is out of this HwBlob
+ */
public native final byte getInt8(long offset);
+ /**
+ * @param offset offset to unmarshall a short from
+ * @return the unmarshalled short value
+ * @throws IndexOutOfBoundsException when offset is out of this HwBlob
+ */
public native final short getInt16(long offset);
+ /**
+ * @param offset offset to unmarshall an int from
+ * @return the unmarshalled int value
+ * @throws IndexOutOfBoundsException when offset is out of this HwBlob
+ */
public native final int getInt32(long offset);
+ /**
+ * @param offset offset to unmarshall a long from
+ * @return the unmarshalled long value
+ * @throws IndexOutOfBoundsException when offset is out of this HwBlob
+ */
public native final long getInt64(long offset);
+ /**
+ * @param offset offset to unmarshall a float from
+ * @return the unmarshalled float value
+ * @throws IndexOutOfBoundsException when offset is out of this HwBlob
+ */
public native final float getFloat(long offset);
+ /**
+ * @param offset offset to unmarshall a double from
+ * @return the unmarshalled double value
+ * @throws IndexOutOfBoundsException when offset is out of this HwBlob
+ */
public native final double getDouble(long offset);
+ /**
+ * @param offset offset to unmarshall a string from
+ * @return the unmarshalled string value
+ * @throws IndexOutOfBoundsException when offset is out of this HwBlob
+ */
public native final String getString(long offset);
/**
- The copyTo... methods copy the blob's data, starting from the given
- byte offset, into the array. A total of "size" _elements_ are copied.
+ * Copy the blobs data starting from the given byte offset into the range, copying
+ * a total of size elements.
+ *
+ * @param offset starting location in blob
+ * @param array destination array
+ * @param size total number of elements to copy
+ * @throws IllegalArgumentException array.length < size
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jboolean)] out of the blob.
*/
public native final void copyToBoolArray(long offset, boolean[] array, int size);
+ /**
+ * Copy the blobs data starting from the given byte offset into the range, copying
+ * a total of size elements.
+ *
+ * @param offset starting location in blob
+ * @param array destination array
+ * @param size total number of elements to copy
+ * @throws IllegalArgumentException array.length < size
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jbyte)] out of the blob.
+ */
public native final void copyToInt8Array(long offset, byte[] array, int size);
+ /**
+ * Copy the blobs data starting from the given byte offset into the range, copying
+ * a total of size elements.
+ *
+ * @param offset starting location in blob
+ * @param array destination array
+ * @param size total number of elements to copy
+ * @throws IllegalArgumentException array.length < size
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jshort)] out of the blob.
+ */
public native final void copyToInt16Array(long offset, short[] array, int size);
+ /**
+ * Copy the blobs data starting from the given byte offset into the range, copying
+ * a total of size elements.
+ *
+ * @param offset starting location in blob
+ * @param array destination array
+ * @param size total number of elements to copy
+ * @throws IllegalArgumentException array.length < size
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jint)] out of the blob.
+ */
public native final void copyToInt32Array(long offset, int[] array, int size);
+ /**
+ * Copy the blobs data starting from the given byte offset into the range, copying
+ * a total of size elements.
+ *
+ * @param offset starting location in blob
+ * @param array destination array
+ * @param size total number of elements to copy
+ * @throws IllegalArgumentException array.length < size
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jlong)] out of the blob.
+ */
public native final void copyToInt64Array(long offset, long[] array, int size);
+ /**
+ * Copy the blobs data starting from the given byte offset into the range, copying
+ * a total of size elements.
+ *
+ * @param offset starting location in blob
+ * @param array destination array
+ * @param size total number of elements to copy
+ * @throws IllegalArgumentException array.length < size
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jfloat)] out of the blob.
+ */
public native final void copyToFloatArray(long offset, float[] array, int size);
+ /**
+ * Copy the blobs data starting from the given byte offset into the range, copying
+ * a total of size elements.
+ *
+ * @param offset starting location in blob
+ * @param array destination array
+ * @param size total number of elements to copy
+ * @throws IllegalArgumentException array.length < size
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jdouble)] out of the blob.
+ */
public native final void copyToDoubleArray(long offset, double[] array, int size);
+ /**
+ * Writes a boolean value at an offset.
+ *
+ * @param offset location to write value
+ * @param x value to write
+ * @throws IndexOutOfBoundsException when [offset, offset + sizeof(jboolean)] is out of range
+ */
public native final void putBool(long offset, boolean x);
+ /**
+ * Writes a byte value at an offset.
+ *
+ * @param offset location to write value
+ * @param x value to write
+ * @throws IndexOutOfBoundsException when [offset, offset + sizeof(jbyte)] is out of range
+ */
public native final void putInt8(long offset, byte x);
+ /**
+ * Writes a short value at an offset.
+ *
+ * @param offset location to write value
+ * @param x value to write
+ * @throws IndexOutOfBoundsException when [offset, offset + sizeof(jshort)] is out of range
+ */
public native final void putInt16(long offset, short x);
+ /**
+ * Writes a int value at an offset.
+ *
+ * @param offset location to write value
+ * @param x value to write
+ * @throws IndexOutOfBoundsException when [offset, offset + sizeof(jint)] is out of range
+ */
public native final void putInt32(long offset, int x);
+ /**
+ * Writes a long value at an offset.
+ *
+ * @param offset location to write value
+ * @param x value to write
+ * @throws IndexOutOfBoundsException when [offset, offset + sizeof(jlong)] is out of range
+ */
public native final void putInt64(long offset, long x);
+ /**
+ * Writes a float value at an offset.
+ *
+ * @param offset location to write value
+ * @param x value to write
+ * @throws IndexOutOfBoundsException when [offset, offset + sizeof(jfloat)] is out of range
+ */
public native final void putFloat(long offset, float x);
+ /**
+ * Writes a double value at an offset.
+ *
+ * @param offset location to write value
+ * @param x value to write
+ * @throws IndexOutOfBoundsException when [offset, offset + sizeof(jdouble)] is out of range
+ */
public native final void putDouble(long offset, double x);
+ /**
+ * Writes a string value at an offset.
+ *
+ * @param offset location to write value
+ * @param x value to write
+ * @throws IndexOutOfBoundsException when [offset, offset + sizeof(jstring)] is out of range
+ */
public native final void putString(long offset, String x);
+ /**
+ * Put a boolean array contiguously at an offset in the blob.
+ *
+ * @param offset location to write values
+ * @param x array to write
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jboolean)] out of the blob.
+ */
public native final void putBoolArray(long offset, boolean[] x);
+ /**
+ * Put a byte array contiguously at an offset in the blob.
+ *
+ * @param offset location to write values
+ * @param x array to write
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jbyte)] out of the blob.
+ */
public native final void putInt8Array(long offset, byte[] x);
+ /**
+ * Put a short array contiguously at an offset in the blob.
+ *
+ * @param offset location to write values
+ * @param x array to write
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jshort)] out of the blob.
+ */
public native final void putInt16Array(long offset, short[] x);
+ /**
+ * Put a int array contiguously at an offset in the blob.
+ *
+ * @param offset location to write values
+ * @param x array to write
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jint)] out of the blob.
+ */
public native final void putInt32Array(long offset, int[] x);
+ /**
+ * Put a long array contiguously at an offset in the blob.
+ *
+ * @param offset location to write values
+ * @param x array to write
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jlong)] out of the blob.
+ */
public native final void putInt64Array(long offset, long[] x);
+ /**
+ * Put a float array contiguously at an offset in the blob.
+ *
+ * @param offset location to write values
+ * @param x array to write
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jfloat)] out of the blob.
+ */
public native final void putFloatArray(long offset, float[] x);
+ /**
+ * Put a double array contiguously at an offset in the blob.
+ *
+ * @param offset location to write values
+ * @param x array to write
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jdouble)] out of the blob.
+ */
public native final void putDoubleArray(long offset, double[] x);
+ /**
+ * Write another HwBlob into this blob at the specified location.
+ *
+ * @param offset location to write value
+ * @param blob data to write
+ * @throws IndexOutOfBoundsException if [offset, offset + blob's size] outside of the range of
+ * this blob.
+ */
public native final void putBlob(long offset, HwBlob blob);
+ /**
+ * @return current handle of HwBlob for reference in a parcelled binder transaction
+ */
public native final long handle();
+ /**
+ * Convert a primitive to a wrapped array for boolean.
+ *
+ * @param array from array
+ * @return transformed array
+ */
public static Boolean[] wrapArray(@NonNull boolean[] array) {
final int n = array.length;
Boolean[] wrappedArray = new Boolean[n];
@@ -85,6 +320,12 @@
return wrappedArray;
}
+ /**
+ * Convert a primitive to a wrapped array for long.
+ *
+ * @param array from array
+ * @return transformed array
+ */
public static Long[] wrapArray(@NonNull long[] array) {
final int n = array.length;
Long[] wrappedArray = new Long[n];
@@ -94,6 +335,12 @@
return wrappedArray;
}
+ /**
+ * Convert a primitive to a wrapped array for byte.
+ *
+ * @param array from array
+ * @return transformed array
+ */
public static Byte[] wrapArray(@NonNull byte[] array) {
final int n = array.length;
Byte[] wrappedArray = new Byte[n];
@@ -103,6 +350,12 @@
return wrappedArray;
}
+ /**
+ * Convert a primitive to a wrapped array for short.
+ *
+ * @param array from array
+ * @return transformed array
+ */
public static Short[] wrapArray(@NonNull short[] array) {
final int n = array.length;
Short[] wrappedArray = new Short[n];
@@ -112,6 +365,12 @@
return wrappedArray;
}
+ /**
+ * Convert a primitive to a wrapped array for int.
+ *
+ * @param array from array
+ * @return transformed array
+ */
public static Integer[] wrapArray(@NonNull int[] array) {
final int n = array.length;
Integer[] wrappedArray = new Integer[n];
@@ -121,6 +380,12 @@
return wrappedArray;
}
+ /**
+ * Convert a primitive to a wrapped array for float.
+ *
+ * @param array from array
+ * @return transformed array
+ */
public static Float[] wrapArray(@NonNull float[] array) {
final int n = array.length;
Float[] wrappedArray = new Float[n];
@@ -130,6 +395,12 @@
return wrappedArray;
}
+ /**
+ * Convert a primitive to a wrapped array for double.
+ *
+ * @param array from array
+ * @return transformed array
+ */
public static Double[] wrapArray(@NonNull double[] array) {
final int n = array.length;
Double[] wrappedArray = new Double[n];
diff --git a/android/os/HwParcel.java b/android/os/HwParcel.java
index 4ba1144..0eb62c9 100644
--- a/android/os/HwParcel.java
+++ b/android/os/HwParcel.java
@@ -16,17 +16,32 @@
package android.os;
-import java.util.ArrayList;
-import java.util.Arrays;
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
import libcore.util.NativeAllocationRegistry;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+
/** @hide */
+@SystemApi
public class HwParcel {
private static final String TAG = "HwParcel";
+ @IntDef(prefix = { "STATUS_" }, value = {
+ STATUS_SUCCESS,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Status {}
+
+ /**
+ * Success return error for a transaction. Written to parcels
+ * using writeStatus.
+ */
public static final int STATUS_SUCCESS = 0;
- public static final int STATUS_ERROR = -1;
private static final NativeAllocationRegistry sNativeRegistry;
@@ -38,6 +53,9 @@
mNativeContext);
}
+ /**
+ * Creates an initialized and empty parcel.
+ */
public HwParcel() {
native_setup(true /* allocate */);
@@ -46,25 +64,106 @@
mNativeContext);
}
+ /**
+ * Writes an interface token into the parcel used to verify that
+ * a transaction has made it to the write type of interface.
+ *
+ * @param interfaceName fully qualified name of interface message
+ * is being sent to.
+ */
public native final void writeInterfaceToken(String interfaceName);
+ /**
+ * Writes a boolean value to the end of the parcel.
+ * @param val to write
+ */
public native final void writeBool(boolean val);
+ /**
+ * Writes a byte value to the end of the parcel.
+ * @param val to write
+ */
public native final void writeInt8(byte val);
+ /**
+ * Writes a short value to the end of the parcel.
+ * @param val to write
+ */
public native final void writeInt16(short val);
+ /**
+ * Writes a int value to the end of the parcel.
+ * @param val to write
+ */
public native final void writeInt32(int val);
+ /**
+ * Writes a long value to the end of the parcel.
+ * @param val to write
+ */
public native final void writeInt64(long val);
+ /**
+ * Writes a float value to the end of the parcel.
+ * @param val to write
+ */
public native final void writeFloat(float val);
+ /**
+ * Writes a double value to the end of the parcel.
+ * @param val to write
+ */
public native final void writeDouble(double val);
+ /**
+ * Writes a String value to the end of the parcel.
+ *
+ * Note, this will be converted to UTF-8 when it is written.
+ *
+ * @param val to write
+ */
public native final void writeString(String val);
+ /**
+ * Writes an array of boolean values to the end of the parcel.
+ * @param val to write
+ */
private native final void writeBoolVector(boolean[] val);
+ /**
+ * Writes an array of byte values to the end of the parcel.
+ * @param val to write
+ */
private native final void writeInt8Vector(byte[] val);
+ /**
+ * Writes an array of short values to the end of the parcel.
+ * @param val to write
+ */
private native final void writeInt16Vector(short[] val);
+ /**
+ * Writes an array of int values to the end of the parcel.
+ * @param val to write
+ */
private native final void writeInt32Vector(int[] val);
+ /**
+ * Writes an array of long values to the end of the parcel.
+ * @param val to write
+ */
private native final void writeInt64Vector(long[] val);
+ /**
+ * Writes an array of float values to the end of the parcel.
+ * @param val to write
+ */
private native final void writeFloatVector(float[] val);
+ /**
+ * Writes an array of double values to the end of the parcel.
+ * @param val to write
+ */
private native final void writeDoubleVector(double[] val);
+ /**
+ * Writes an array of String values to the end of the parcel.
+ *
+ * Note, these will be converted to UTF-8 as they are written.
+ *
+ * @param val to write
+ */
private native final void writeStringVector(String[] val);
+ /**
+ * Helper method to write a list of Booleans to val.
+ * @param val list to write
+ */
public final void writeBoolVector(ArrayList<Boolean> val) {
final int n = val.size();
boolean[] array = new boolean[n];
@@ -75,6 +174,10 @@
writeBoolVector(array);
}
+ /**
+ * Helper method to write a list of Booleans to the end of the parcel.
+ * @param val list to write
+ */
public final void writeInt8Vector(ArrayList<Byte> val) {
final int n = val.size();
byte[] array = new byte[n];
@@ -85,6 +188,10 @@
writeInt8Vector(array);
}
+ /**
+ * Helper method to write a list of Shorts to the end of the parcel.
+ * @param val list to write
+ */
public final void writeInt16Vector(ArrayList<Short> val) {
final int n = val.size();
short[] array = new short[n];
@@ -95,6 +202,10 @@
writeInt16Vector(array);
}
+ /**
+ * Helper method to write a list of Integers to the end of the parcel.
+ * @param val list to write
+ */
public final void writeInt32Vector(ArrayList<Integer> val) {
final int n = val.size();
int[] array = new int[n];
@@ -105,6 +216,10 @@
writeInt32Vector(array);
}
+ /**
+ * Helper method to write a list of Longs to the end of the parcel.
+ * @param val list to write
+ */
public final void writeInt64Vector(ArrayList<Long> val) {
final int n = val.size();
long[] array = new long[n];
@@ -115,6 +230,10 @@
writeInt64Vector(array);
}
+ /**
+ * Helper method to write a list of Floats to the end of the parcel.
+ * @param val list to write
+ */
public final void writeFloatVector(ArrayList<Float> val) {
final int n = val.size();
float[] array = new float[n];
@@ -125,6 +244,10 @@
writeFloatVector(array);
}
+ /**
+ * Helper method to write a list of Doubles to the end of the parcel.
+ * @param val list to write
+ */
public final void writeDoubleVector(ArrayList<Double> val) {
final int n = val.size();
double[] array = new double[n];
@@ -135,93 +258,272 @@
writeDoubleVector(array);
}
+ /**
+ * Helper method to write a list of Strings to the end of the parcel.
+ * @param val list to write
+ */
public final void writeStringVector(ArrayList<String> val) {
writeStringVector(val.toArray(new String[val.size()]));
}
+ /**
+ * Write a hwbinder object to the end of the parcel.
+ * @param binder value to write
+ */
public native final void writeStrongBinder(IHwBinder binder);
+ /**
+ * Checks to make sure that the interface name matches the name written by the parcel
+ * sender by writeInterfaceToken
+ *
+ * @throws SecurityException interface doesn't match
+ */
public native final void enforceInterface(String interfaceName);
+
+ /**
+ * Reads a boolean value from the current location in the parcel.
+ * @return value parsed from the parcel
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final boolean readBool();
+ /**
+ * Reads a byte value from the current location in the parcel.
+ * @return value parsed from the parcel
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final byte readInt8();
+ /**
+ * Reads a short value from the current location in the parcel.
+ * @return value parsed from the parcel
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final short readInt16();
+ /**
+ * Reads a int value from the current location in the parcel.
+ * @return value parsed from the parcel
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final int readInt32();
+ /**
+ * Reads a long value from the current location in the parcel.
+ * @return value parsed from the parcel
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final long readInt64();
+ /**
+ * Reads a float value from the current location in the parcel.
+ * @return value parsed from the parcel
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final float readFloat();
+ /**
+ * Reads a double value from the current location in the parcel.
+ * @return value parsed from the parcel
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final double readDouble();
+ /**
+ * Reads a String value from the current location in the parcel.
+ * @return value parsed from the parcel
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final String readString();
+ /**
+ * Reads an array of boolean values from the parcel.
+ * @return array of parsed values
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
private native final boolean[] readBoolVectorAsArray();
+ /**
+ * Reads an array of byte values from the parcel.
+ * @return array of parsed values
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
private native final byte[] readInt8VectorAsArray();
+ /**
+ * Reads an array of short values from the parcel.
+ * @return array of parsed values
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
private native final short[] readInt16VectorAsArray();
+ /**
+ * Reads an array of int values from the parcel.
+ * @return array of parsed values
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
private native final int[] readInt32VectorAsArray();
+ /**
+ * Reads an array of long values from the parcel.
+ * @return array of parsed values
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
private native final long[] readInt64VectorAsArray();
+ /**
+ * Reads an array of float values from the parcel.
+ * @return array of parsed values
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
private native final float[] readFloatVectorAsArray();
+ /**
+ * Reads an array of double values from the parcel.
+ * @return array of parsed values
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
private native final double[] readDoubleVectorAsArray();
+ /**
+ * Reads an array of String values from the parcel.
+ * @return array of parsed values
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
private native final String[] readStringVectorAsArray();
+ /**
+ * Convenience method to read a Boolean vector as an ArrayList.
+ * @return array of parsed values.
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public final ArrayList<Boolean> readBoolVector() {
Boolean[] array = HwBlob.wrapArray(readBoolVectorAsArray());
return new ArrayList<Boolean>(Arrays.asList(array));
}
+ /**
+ * Convenience method to read a Byte vector as an ArrayList.
+ * @return array of parsed values.
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public final ArrayList<Byte> readInt8Vector() {
Byte[] array = HwBlob.wrapArray(readInt8VectorAsArray());
return new ArrayList<Byte>(Arrays.asList(array));
}
+ /**
+ * Convenience method to read a Short vector as an ArrayList.
+ * @return array of parsed values.
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public final ArrayList<Short> readInt16Vector() {
Short[] array = HwBlob.wrapArray(readInt16VectorAsArray());
return new ArrayList<Short>(Arrays.asList(array));
}
+ /**
+ * Convenience method to read a Integer vector as an ArrayList.
+ * @return array of parsed values.
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public final ArrayList<Integer> readInt32Vector() {
Integer[] array = HwBlob.wrapArray(readInt32VectorAsArray());
return new ArrayList<Integer>(Arrays.asList(array));
}
+ /**
+ * Convenience method to read a Long vector as an ArrayList.
+ * @return array of parsed values.
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public final ArrayList<Long> readInt64Vector() {
Long[] array = HwBlob.wrapArray(readInt64VectorAsArray());
return new ArrayList<Long>(Arrays.asList(array));
}
+ /**
+ * Convenience method to read a Float vector as an ArrayList.
+ * @return array of parsed values.
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public final ArrayList<Float> readFloatVector() {
Float[] array = HwBlob.wrapArray(readFloatVectorAsArray());
return new ArrayList<Float>(Arrays.asList(array));
}
+ /**
+ * Convenience method to read a Double vector as an ArrayList.
+ * @return array of parsed values.
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public final ArrayList<Double> readDoubleVector() {
Double[] array = HwBlob.wrapArray(readDoubleVectorAsArray());
return new ArrayList<Double>(Arrays.asList(array));
}
+ /**
+ * Convenience method to read a String vector as an ArrayList.
+ * @return array of parsed values.
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public final ArrayList<String> readStringVector() {
return new ArrayList<String>(Arrays.asList(readStringVectorAsArray()));
}
+ /**
+ * Reads a strong binder value from the parcel.
+ * @return binder object read from parcel or null if no binder can be read
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final IHwBinder readStrongBinder();
- // Handle is stored as part of the blob.
+ /**
+ * Read opaque segment of data as a blob.
+ * @return blob of size expectedSize
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final HwBlob readBuffer(long expectedSize);
+ /**
+ * Read a buffer written using scatter gather.
+ *
+ * @param expectedSize size that buffer should be
+ * @param parentHandle handle from which to read the embedded buffer
+ * @param offset offset into parent
+ * @param nullable whether or not to allow for a null return
+ * @return blob of data with size expectedSize
+ * @throws NoSuchElementException if an embedded buffer is not available to read
+ * @throws IllegalArgumentException if expectedSize < 0
+ * @throws NullPointerException if the transaction specified the blob to be null
+ * but nullable is false
+ */
public native final HwBlob readEmbeddedBuffer(
long expectedSize, long parentHandle, long offset,
boolean nullable);
+ /**
+ * Write a buffer into the transaction.
+ * @param blob blob to write into the parcel.
+ */
public native final void writeBuffer(HwBlob blob);
-
+ /**
+ * Write a status value into the blob.
+ * @param status value to write
+ */
public native final void writeStatus(int status);
+ /**
+ * @throws IllegalArgumentException if a success vaue cannot be read
+ * @throws RemoteException if success value indicates a transaction error
+ */
public native final void verifySuccess();
+ /**
+ * Should be called to reduce memory pressure when this object no longer needs
+ * to be written to.
+ */
public native final void releaseTemporaryStorage();
+ /**
+ * Should be called when object is no longer needed to reduce possible memory
+ * pressure if the Java GC does not get to this object in time.
+ */
public native final void release();
+ /**
+ * Sends the parcel to the specified destination.
+ */
public native final void send();
// Returns address of the "freeFunction".
diff --git a/android/os/IHwBinder.java b/android/os/IHwBinder.java
index 619f4dc..ce9f6c1 100644
--- a/android/os/IHwBinder.java
+++ b/android/os/IHwBinder.java
@@ -16,26 +16,47 @@
package android.os;
+import android.annotation.SystemApi;
+
/** @hide */
+@SystemApi
public interface IHwBinder {
// These MUST match their corresponding libhwbinder/IBinder.h definition !!!
+ /** @hide */
public static final int FIRST_CALL_TRANSACTION = 1;
+ /** @hide */
public static final int FLAG_ONEWAY = 1;
+ /** @hide */
public void transact(
int code, HwParcel request, HwParcel reply, int flags)
throws RemoteException;
+ /** @hide */
public IHwInterface queryLocalInterface(String descriptor);
/**
* Interface for receiving a callback when the process hosting a service
* has gone away.
*/
+ @SystemApi
public interface DeathRecipient {
+ /**
+ * Callback for a registered process dying.
+ */
+ @SystemApi
public void serviceDied(long cookie);
}
+ /**
+ * Notifies the death recipient with the cookie when the process containing
+ * this binder dies.
+ */
+ @SystemApi
public boolean linkToDeath(DeathRecipient recipient, long cookie);
+ /**
+ * Unregisters the death recipient from this binder.
+ */
+ @SystemApi
public boolean unlinkToDeath(DeathRecipient recipient);
}
diff --git a/android/os/IHwInterface.java b/android/os/IHwInterface.java
index 7c5ac6f..a2f59a9 100644
--- a/android/os/IHwInterface.java
+++ b/android/os/IHwInterface.java
@@ -16,7 +16,13 @@
package android.os;
+import android.annotation.SystemApi;
/** @hide */
+@SystemApi
public interface IHwInterface {
+ /**
+ * Returns the binder object that corresponds to an interface.
+ */
+ @SystemApi
public IHwBinder asBinder();
}
diff --git a/android/os/PackageManagerPerfTest.java b/android/os/PackageManagerPerfTest.java
new file mode 100644
index 0000000..145fbcd
--- /dev/null
+++ b/android/os/PackageManagerPerfTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2017 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.os;
+
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class PackageManagerPerfTest {
+ private static final String PERMISSION_NAME_EXISTS =
+ "com.android.perftests.core.TestPermission";
+ private static final String PERMISSION_NAME_DOESNT_EXIST =
+ "com.android.perftests.core.TestBadPermission";
+ private static final ComponentName TEST_ACTIVITY =
+ new ComponentName("com.android.perftests.core", "android.perftests.utils.StubActivity");
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testCheckPermissionExists() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final PackageManager pm = InstrumentationRegistry.getTargetContext().getPackageManager();
+ final String packageName = TEST_ACTIVITY.getPackageName();
+
+ while (state.keepRunning()) {
+ int ret = pm.checkPermission(PERMISSION_NAME_EXISTS, packageName);
+ }
+ }
+
+ @Test
+ public void testCheckPermissionDoesntExist() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final PackageManager pm = InstrumentationRegistry.getTargetContext().getPackageManager();
+ final String packageName = TEST_ACTIVITY.getPackageName();
+
+ while (state.keepRunning()) {
+ int ret = pm.checkPermission(PERMISSION_NAME_DOESNT_EXIST, packageName);
+ }
+ }
+
+ @Test
+ public void testQueryIntentActivities() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final PackageManager pm = InstrumentationRegistry.getTargetContext().getPackageManager();
+ final Intent intent = new Intent("com.android.perftests.core.PERFTEST");
+
+ while (state.keepRunning()) {
+ pm.queryIntentActivities(intent, 0);
+ }
+ }
+
+ @Test
+ public void testGetPackageInfo() throws Exception {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final PackageManager pm = InstrumentationRegistry.getTargetContext().getPackageManager();
+ final String packageName = TEST_ACTIVITY.getPackageName();
+
+ while (state.keepRunning()) {
+ pm.getPackageInfo(packageName, 0);
+ }
+ }
+
+ @Test
+ public void testGetApplicationInfo() throws Exception {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final PackageManager pm = InstrumentationRegistry.getTargetContext().getPackageManager();
+ final String packageName = TEST_ACTIVITY.getPackageName();
+
+ while (state.keepRunning()) {
+ pm.getApplicationInfo(packageName, 0);
+ }
+ }
+
+ @Test
+ public void testGetActivityInfo() throws Exception {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final PackageManager pm = InstrumentationRegistry.getTargetContext().getPackageManager();
+
+ while (state.keepRunning()) {
+ pm.getActivityInfo(TEST_ACTIVITY, 0);
+ }
+ }
+}
diff --git a/android/os/PersistableBundle.java b/android/os/PersistableBundle.java
index 3ed5b17..40eceb8 100644
--- a/android/os/PersistableBundle.java
+++ b/android/os/PersistableBundle.java
@@ -18,6 +18,7 @@
import android.annotation.Nullable;
import android.util.ArrayMap;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.util.XmlUtils;
@@ -321,4 +322,21 @@
}
return mMap.toString();
}
+
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ if (mParcelledData != null) {
+ if (isEmptyParcel()) {
+ proto.write(PersistableBundleProto.PARCELLED_DATA_SIZE, 0);
+ } else {
+ proto.write(PersistableBundleProto.PARCELLED_DATA_SIZE, mParcelledData.dataSize());
+ }
+ } else {
+ proto.write(PersistableBundleProto.MAP_DATA, mMap.toString());
+ }
+
+ proto.end(token);
+ }
}
diff --git a/android/os/PowerManager.java b/android/os/PowerManager.java
index cd6d41b..3d17ffb 100644
--- a/android/os/PowerManager.java
+++ b/android/os/PowerManager.java
@@ -23,6 +23,7 @@
import android.annotation.SystemService;
import android.content.Context;
import android.util.Log;
+import android.util.proto.ProtoOutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -565,6 +566,42 @@
int OPTIONAL_SENSORS = 13;
}
+ /**
+ * Either the location providers shouldn't be affected by battery saver,
+ * or battery saver is off.
+ */
+ public static final int LOCATION_MODE_NO_CHANGE = 0;
+
+ /**
+ * In this mode, the GPS based location provider should be disabled when battery saver is on and
+ * the device is non-interactive.
+ */
+ public static final int LOCATION_MODE_GPS_DISABLED_WHEN_SCREEN_OFF = 1;
+
+ /**
+ * All location providers should be disabled when battery saver is on and
+ * the device is non-interactive.
+ */
+ public static final int LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF = 2;
+
+ /**
+ * In this mode, all the location providers will be kept available, but location fixes
+ * should only be provided to foreground apps.
+ */
+ public static final int LOCATION_MODE_FOREGROUND_ONLY = 3;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"LOCATION_MODE_"}, value = {
+ LOCATION_MODE_NO_CHANGE,
+ LOCATION_MODE_GPS_DISABLED_WHEN_SCREEN_OFF,
+ LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF,
+ LOCATION_MODE_FOREGROUND_ONLY,
+ })
+ public @interface LocationPowerSaveMode {}
+
final Context mContext;
final IPowerManager mService;
final Handler mHandler;
@@ -964,24 +1001,6 @@
return false;
}
- /**
- * Sets the brightness of the backlights (screen, keyboard, button).
- * <p>
- * Requires the {@link android.Manifest.permission#DEVICE_POWER} permission.
- * </p>
- *
- * @param brightness The brightness value from 0 to 255.
- *
- * @hide Requires signature permission.
- */
- public void setBacklightBrightness(int brightness) {
- try {
- mService.setTemporaryScreenBrightnessSettingOverride(brightness);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
/**
* Returns true if the specified wake lock level is supported.
*
@@ -1143,6 +1162,24 @@
}
/**
+ * Returns how location features should behave when battery saver is on. When battery saver
+ * is off, this will always return {@link #LOCATION_MODE_NO_CHANGE}.
+ *
+ * <p>This API is normally only useful for components that provide location features.
+ *
+ * @see #isPowerSaveMode()
+ * @see #ACTION_POWER_SAVE_MODE_CHANGED
+ */
+ @LocationPowerSaveMode
+ public int getLocationPowerSaveMode() {
+ final PowerSaveState powerSaveState = getPowerSaveState(ServiceType.GPS);
+ if (!powerSaveState.globalBatterySaverEnabled) {
+ return LOCATION_MODE_NO_CHANGE;
+ }
+ return powerSaveState.gpsMode;
+ }
+
+ /**
* Returns true if the device is currently in idle mode. This happens when a device
* has been sitting unused and unmoving for a sufficiently long period of time, so that
* it decides to go into a lower power-use state. This may involve things like turning
@@ -1598,6 +1635,21 @@
}
}
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ synchronized (mToken) {
+ final long token = proto.start(fieldId);
+ proto.write(PowerManagerProto.WakeLockProto.HEX_STRING,
+ Integer.toHexString(System.identityHashCode(this)));
+ proto.write(PowerManagerProto.WakeLockProto.HELD, mHeld);
+ proto.write(PowerManagerProto.WakeLockProto.INTERNAL_COUNT, mInternalCount);
+ if (mWorkSource != null) {
+ mWorkSource.writeToProto(proto, PowerManagerProto.WakeLockProto.WORK_SOURCE);
+ }
+ proto.end(token);
+ }
+ }
+
/**
* Wraps a Runnable such that this method immediately acquires the wake lock and then
* once the Runnable is done the wake lock is released.
diff --git a/android/os/PowerManagerInternal.java b/android/os/PowerManagerInternal.java
index 3ef0961..c7d89b0 100644
--- a/android/os/PowerManagerInternal.java
+++ b/android/os/PowerManagerInternal.java
@@ -71,6 +71,24 @@
}
/**
+ * Converts platform constants to proto enums.
+ */
+ public static int wakefulnessToProtoEnum(int wakefulness) {
+ switch (wakefulness) {
+ case WAKEFULNESS_ASLEEP:
+ return PowerManagerInternalProto.WAKEFULNESS_ASLEEP;
+ case WAKEFULNESS_AWAKE:
+ return PowerManagerInternalProto.WAKEFULNESS_AWAKE;
+ case WAKEFULNESS_DREAMING:
+ return PowerManagerInternalProto.WAKEFULNESS_DREAMING;
+ case WAKEFULNESS_DOZING:
+ return PowerManagerInternalProto.WAKEFULNESS_DOZING;
+ default:
+ return wakefulness;
+ }
+ }
+
+ /**
* Returns true if the wakefulness state represents an interactive state
* as defined by {@link android.os.PowerManager#isInteractive}.
*/
diff --git a/android/os/Process.java b/android/os/Process.java
index 0874d93..6833908 100644
--- a/android/os/Process.java
+++ b/android/os/Process.java
@@ -143,7 +143,7 @@
* Defines the UID/GID for the WebView zygote process.
* @hide
*/
- public static final int WEBVIEW_ZYGOTE_UID = 1051;
+ public static final int WEBVIEW_ZYGOTE_UID = 1053;
/**
* Defines the UID used for resource tracking for OTA updates.
@@ -151,6 +151,12 @@
*/
public static final int OTA_UPDATE_UID = 1061;
+ /**
+ * Defines the UID used for incidentd.
+ * @hide
+ */
+ public static final int INCIDENTD_UID = 1067;
+
/** {@hide} */
public static final int NOBODY_UID = 9999;
@@ -269,6 +275,15 @@
public static final int THREAD_PRIORITY_URGENT_DISPLAY = -8;
/**
+ * Standard priority of video threads. Applications can not normally
+ * change to this priority.
+ * Use with {@link #setThreadPriority(int)} and
+ * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
+ * {@link java.lang.Thread} class.
+ */
+ public static final int THREAD_PRIORITY_VIDEO = -10;
+
+ /**
* Standard priority of audio threads. Applications can not normally
* change to this priority.
* Use with {@link #setThreadPriority(int)} and
@@ -559,6 +574,14 @@
}
/**
+ * Returns whether the given uid belongs to a system core component or not.
+ * @hide
+ */
+ public static boolean isCoreUid(int uid) {
+ return UserHandle.isCore(uid);
+ }
+
+ /**
* Returns whether the given uid belongs to an application.
* @param uid A kernel uid.
* @return Whether the uid corresponds to an application sandbox running in
diff --git a/android/os/PssPerfTest.java b/android/os/PssPerfTest.java
new file mode 100644
index 0000000..400115d
--- /dev/null
+++ b/android/os/PssPerfTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 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.os;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class PssPerfTest {
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testPss() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ Debug.getPss();
+ }
+ }
+}
diff --git a/android/os/RecoverySystem.java b/android/os/RecoverySystem.java
index 673a8ba..3e8e885 100644
--- a/android/os/RecoverySystem.java
+++ b/android/os/RecoverySystem.java
@@ -61,6 +61,7 @@
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
@@ -101,6 +102,9 @@
private static final String ACTION_EUICC_FACTORY_RESET =
"com.android.internal.action.EUICC_FACTORY_RESET";
+ /** used in {@link #wipeEuiccData} as package name of callback intent */
+ private static final String PACKAGE_NAME_WIPING_EUICC_DATA_CALLBACK = "android";
+
/**
* The recovery image uses this file to identify the location (i.e. blocks)
* of an OTA package on the /data partition. The block map file is
@@ -751,7 +755,9 @@
// Block until the ordered broadcast has completed.
condition.block();
- wipeEuiccData(context, wipeEuicc);
+ if (wipeEuicc) {
+ wipeEuiccData(context, PACKAGE_NAME_WIPING_EUICC_DATA_CALLBACK);
+ }
String shutdownArg = null;
if (shutdown) {
@@ -767,19 +773,27 @@
bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);
}
- private static void wipeEuiccData(Context context, final boolean isWipeEuicc) {
+ /**
+ * Returns whether wipe Euicc data successfully or not.
+ *
+ * @param packageName the package name of the caller app.
+ *
+ * @hide
+ */
+ public static boolean wipeEuiccData(Context context, final String packageName) {
ContentResolver cr = context.getContentResolver();
if (Settings.Global.getInt(cr, Settings.Global.EUICC_PROVISIONED, 0) == 0) {
// If the eUICC isn't provisioned, there's no reason to either wipe or retain profiles,
// as there's nothing to wipe nor retain.
Log.d(TAG, "Skipping eUICC wipe/retain as it is not provisioned");
- return;
+ return true;
}
EuiccManager euiccManager = (EuiccManager) context.getSystemService(
Context.EUICC_SERVICE);
if (euiccManager != null && euiccManager.isEnabled()) {
CountDownLatch euiccFactoryResetLatch = new CountDownLatch(1);
+ final AtomicBoolean wipingSucceeded = new AtomicBoolean(false);
BroadcastReceiver euiccWipeFinishReceiver = new BroadcastReceiver() {
@Override
@@ -788,19 +802,11 @@
if (getResultCode() != EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK) {
int detailedCode = intent.getIntExtra(
EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE, 0);
- if (isWipeEuicc) {
- Log.e(TAG, "Error wiping euicc data, Detailed code = "
- + detailedCode);
- } else {
- Log.e(TAG, "Error retaining euicc data, Detailed code = "
- + detailedCode);
- }
+ Log.e(TAG, "Error wiping euicc data, Detailed code = "
+ + detailedCode);
} else {
- if (isWipeEuicc) {
- Log.d(TAG, "Successfully wiped euicc data.");
- } else {
- Log.d(TAG, "Successfully retained euicc data.");
- }
+ Log.d(TAG, "Successfully wiped euicc data.");
+ wipingSucceeded.set(true /* newValue */);
}
euiccFactoryResetLatch.countDown();
}
@@ -808,7 +814,7 @@
};
Intent intent = new Intent(ACTION_EUICC_FACTORY_RESET);
- intent.setPackage("android");
+ intent.setPackage(packageName);
PendingIntent callbackIntent = PendingIntent.getBroadcastAsUser(
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT, UserHandle.SYSTEM);
IntentFilter filterConsent = new IntentFilter();
@@ -818,11 +824,7 @@
Handler euiccHandler = new Handler(euiccHandlerThread.getLooper());
context.getApplicationContext()
.registerReceiver(euiccWipeFinishReceiver, filterConsent, null, euiccHandler);
- if (isWipeEuicc) {
- euiccManager.eraseSubscriptions(callbackIntent);
- } else {
- euiccManager.retainSubscriptionsForFactoryReset(callbackIntent);
- }
+ euiccManager.eraseSubscriptions(callbackIntent);
try {
long waitingTimeMillis = Settings.Global.getLong(
context.getContentResolver(),
@@ -834,22 +836,19 @@
waitingTimeMillis = MAX_EUICC_FACTORY_RESET_TIMEOUT_MILLIS;
}
if (!euiccFactoryResetLatch.await(waitingTimeMillis, TimeUnit.MILLISECONDS)) {
- if (isWipeEuicc) {
- Log.e(TAG, "Timeout wiping eUICC data.");
- } else {
- Log.e(TAG, "Timeout retaining eUICC data.");
- }
+ Log.e(TAG, "Timeout wiping eUICC data.");
+ return false;
}
- context.getApplicationContext().unregisterReceiver(euiccWipeFinishReceiver);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
- if (isWipeEuicc) {
- Log.e(TAG, "Wiping eUICC data interrupted", e);
- } else {
- Log.e(TAG, "Retaining eUICC data interrupted", e);
- }
+ Log.e(TAG, "Wiping eUICC data interrupted", e);
+ return false;
+ } finally {
+ context.getApplicationContext().unregisterReceiver(euiccWipeFinishReceiver);
}
+ return wipingSucceeded.get();
}
+ return false;
}
/** {@hide} */
diff --git a/android/os/StatsDimensionsValue.java b/android/os/StatsDimensionsValue.java
new file mode 100644
index 0000000..257cc52
--- /dev/null
+++ b/android/os/StatsDimensionsValue.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright 2018 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.os;
+
+import android.annotation.SystemApi;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Container for statsd dimension value information, corresponding to a
+ * stats_log.proto's DimensionValue.
+ *
+ * This consists of a field (an int representing a statsd atom field)
+ * and a value (which may be one of a number of types).
+ *
+ * <p>
+ * Only a single value is held, and it is necessarily one of the following types:
+ * {@link String}, int, long, boolean, float,
+ * or tuple (i.e. {@link List} of {@code StatsDimensionsValue}).
+ *
+ * The type of value held can be retrieved using {@link #getValueType()}, which returns one of the
+ * following ints, depending on the type of value:
+ * <ul>
+ * <li>{@link #STRING_VALUE_TYPE}</li>
+ * <li>{@link #INT_VALUE_TYPE}</li>
+ * <li>{@link #LONG_VALUE_TYPE}</li>
+ * <li>{@link #BOOLEAN_VALUE_TYPE}</li>
+ * <li>{@link #FLOAT_VALUE_TYPE}</li>
+ * <li>{@link #TUPLE_VALUE_TYPE}</li>
+ * </ul>
+ * Alternatively, this can be determined using {@link #isValueType(int)} with one of these constants
+ * as a parameter.
+ * The value itself can be retrieved using the correct get...Value() function for its type.
+ *
+ * <p>
+ * The field is always an int, and always exists; it can be obtained using {@link #getField()}.
+ *
+ *
+ * @hide
+ */
+@SystemApi
+public final class StatsDimensionsValue implements Parcelable {
+ private static final String TAG = "StatsDimensionsValue";
+
+ // Values of the value type correspond to stats_log.proto's DimensionValue fields.
+ // Keep constants in sync with services/include/android/os/StatsDimensionsValue.h.
+ /** Indicates that this holds a String. */
+ public static final int STRING_VALUE_TYPE = 2;
+ /** Indicates that this holds an int. */
+ public static final int INT_VALUE_TYPE = 3;
+ /** Indicates that this holds a long. */
+ public static final int LONG_VALUE_TYPE = 4;
+ /** Indicates that this holds a boolean. */
+ public static final int BOOLEAN_VALUE_TYPE = 5;
+ /** Indicates that this holds a float. */
+ public static final int FLOAT_VALUE_TYPE = 6;
+ /** Indicates that this holds a List of StatsDimensionsValues. */
+ public static final int TUPLE_VALUE_TYPE = 7;
+
+ /** Value of a stats_log.proto DimensionsValue.field. */
+ private final int mField;
+
+ /** Type of stats_log.proto DimensionsValue.value, according to the VALUE_TYPEs above. */
+ private final int mValueType;
+
+ /**
+ * Value of a stats_log.proto DimensionsValue.value.
+ * String, Integer, Long, Boolean, Float, or StatsDimensionsValue[].
+ */
+ private final Object mValue; // immutable or array of immutables
+
+ /**
+ * Creates a {@code StatsDimensionValue} from a parcel.
+ *
+ * @hide
+ */
+ public StatsDimensionsValue(Parcel in) {
+ mField = in.readInt();
+ mValueType = in.readInt();
+ mValue = readValueFromParcel(mValueType, in);
+ }
+
+ /**
+ * Return the field, i.e. the tag of a statsd atom.
+ *
+ * @return the field
+ */
+ public int getField() {
+ return mField;
+ }
+
+ /**
+ * Retrieve the String held, if any.
+ *
+ * @return the {@link String} held if {@link #getValueType()} == {@link #STRING_VALUE_TYPE},
+ * null otherwise
+ */
+ public String getStringValue() {
+ try {
+ if (mValueType == STRING_VALUE_TYPE) return (String) mValue;
+ } catch (ClassCastException e) {
+ Slog.w(TAG, "Failed to successfully get value", e);
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve the int held, if any.
+ *
+ * @return the int held if {@link #getValueType()} == {@link #INT_VALUE_TYPE}, 0 otherwise
+ */
+ public int getIntValue() {
+ try {
+ if (mValueType == INT_VALUE_TYPE) return (Integer) mValue;
+ } catch (ClassCastException e) {
+ Slog.w(TAG, "Failed to successfully get value", e);
+ }
+ return 0;
+ }
+
+ /**
+ * Retrieve the long held, if any.
+ *
+ * @return the long held if {@link #getValueType()} == {@link #LONG_VALUE_TYPE}, 0 otherwise
+ */
+ public long getLongValue() {
+ try {
+ if (mValueType == LONG_VALUE_TYPE) return (Long) mValue;
+ } catch (ClassCastException e) {
+ Slog.w(TAG, "Failed to successfully get value", e);
+ }
+ return 0;
+ }
+
+ /**
+ * Retrieve the boolean held, if any.
+ *
+ * @return the boolean held if {@link #getValueType()} == {@link #BOOLEAN_VALUE_TYPE},
+ * false otherwise
+ */
+ public boolean getBooleanValue() {
+ try {
+ if (mValueType == BOOLEAN_VALUE_TYPE) return (Boolean) mValue;
+ } catch (ClassCastException e) {
+ Slog.w(TAG, "Failed to successfully get value", e);
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve the float held, if any.
+ *
+ * @return the float held if {@link #getValueType()} == {@link #FLOAT_VALUE_TYPE}, 0 otherwise
+ */
+ public float getFloatValue() {
+ try {
+ if (mValueType == FLOAT_VALUE_TYPE) return (Float) mValue;
+ } catch (ClassCastException e) {
+ Slog.w(TAG, "Failed to successfully get value", e);
+ }
+ return 0;
+ }
+
+ /**
+ * Retrieve the tuple, in the form of a {@link List} of {@link StatsDimensionsValue}, held,
+ * if any.
+ *
+ * @return the {@link List} of {@link StatsDimensionsValue} held
+ * if {@link #getValueType()} == {@link #TUPLE_VALUE_TYPE},
+ * null otherwise
+ */
+ public List<StatsDimensionsValue> getTupleValueList() {
+ if (mValueType != TUPLE_VALUE_TYPE) {
+ return null;
+ }
+ try {
+ StatsDimensionsValue[] orig = (StatsDimensionsValue[]) mValue;
+ List<StatsDimensionsValue> copy = new ArrayList<>(orig.length);
+ // Shallow copy since StatsDimensionsValue is immutable anyway
+ for (int i = 0; i < orig.length; i++) {
+ copy.add(orig[i]);
+ }
+ return copy;
+ } catch (ClassCastException e) {
+ Slog.w(TAG, "Failed to successfully get value", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the constant representing the type of value stored, namely one of
+ * <ul>
+ * <li>{@link #STRING_VALUE_TYPE}</li>
+ * <li>{@link #INT_VALUE_TYPE}</li>
+ * <li>{@link #LONG_VALUE_TYPE}</li>
+ * <li>{@link #BOOLEAN_VALUE_TYPE}</li>
+ * <li>{@link #FLOAT_VALUE_TYPE}</li>
+ * <li>{@link #TUPLE_VALUE_TYPE}</li>
+ * </ul>
+ *
+ * @return the constant representing the type of value stored
+ */
+ public int getValueType() {
+ return mValueType;
+ }
+
+ /**
+ * Returns whether the type of value stored is equal to the given type.
+ *
+ * @param valueType int representing the type of value stored, as used in {@link #getValueType}
+ * @return true if {@link #getValueType()} is equal to {@code valueType}.
+ */
+ public boolean isValueType(int valueType) {
+ return mValueType == valueType;
+ }
+
+ /**
+ * Returns a String representing the information in this StatsDimensionValue.
+ * No guarantees are made about the format of this String.
+ *
+ * @return String representation
+ *
+ * @hide
+ */
+ // Follows the format of statsd's dimension.h toString.
+ public String toString() {
+ try {
+ StringBuilder sb = new StringBuilder();
+ sb.append(mField);
+ sb.append(":");
+ if (mValueType == TUPLE_VALUE_TYPE) {
+ sb.append("{");
+ StatsDimensionsValue[] sbvs = (StatsDimensionsValue[]) mValue;
+ for (int i = 0; i < sbvs.length; i++) {
+ sb.append(sbvs[i].toString());
+ sb.append("|");
+ }
+ sb.append("}");
+ } else {
+ sb.append(mValue.toString());
+ }
+ return sb.toString();
+ } catch (ClassCastException e) {
+ Slog.w(TAG, "Failed to successfully get value", e);
+ }
+ return "";
+ }
+
+ /**
+ * Parcelable Creator for StatsDimensionsValue.
+ */
+ public static final Parcelable.Creator<StatsDimensionsValue> CREATOR = new
+ Parcelable.Creator<StatsDimensionsValue>() {
+ public StatsDimensionsValue createFromParcel(Parcel in) {
+ return new StatsDimensionsValue(in);
+ }
+
+ public StatsDimensionsValue[] newArray(int size) {
+ return new StatsDimensionsValue[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mField);
+ out.writeInt(mValueType);
+ writeValueToParcel(mValueType, mValue, out, flags);
+ }
+
+ /** Writes mValue to a parcel. Returns true if succeeds. */
+ private static boolean writeValueToParcel(int valueType, Object value, Parcel out, int flags) {
+ try {
+ switch (valueType) {
+ case STRING_VALUE_TYPE:
+ out.writeString((String) value);
+ return true;
+ case INT_VALUE_TYPE:
+ out.writeInt((Integer) value);
+ return true;
+ case LONG_VALUE_TYPE:
+ out.writeLong((Long) value);
+ return true;
+ case BOOLEAN_VALUE_TYPE:
+ out.writeBoolean((Boolean) value);
+ return true;
+ case FLOAT_VALUE_TYPE:
+ out.writeFloat((Float) value);
+ return true;
+ case TUPLE_VALUE_TYPE: {
+ StatsDimensionsValue[] values = (StatsDimensionsValue[]) value;
+ out.writeInt(values.length);
+ for (int i = 0; i < values.length; i++) {
+ values[i].writeToParcel(out, flags);
+ }
+ return true;
+ }
+ default:
+ Slog.w(TAG, "readValue of an impossible type " + valueType);
+ return false;
+ }
+ } catch (ClassCastException e) {
+ Slog.w(TAG, "writeValue cast failed", e);
+ return false;
+ }
+ }
+
+ /** Reads mValue from a parcel. */
+ private static Object readValueFromParcel(int valueType, Parcel parcel) {
+ switch (valueType) {
+ case STRING_VALUE_TYPE:
+ return parcel.readString();
+ case INT_VALUE_TYPE:
+ return parcel.readInt();
+ case LONG_VALUE_TYPE:
+ return parcel.readLong();
+ case BOOLEAN_VALUE_TYPE:
+ return parcel.readBoolean();
+ case FLOAT_VALUE_TYPE:
+ return parcel.readFloat();
+ case TUPLE_VALUE_TYPE: {
+ final int sz = parcel.readInt();
+ StatsDimensionsValue[] values = new StatsDimensionsValue[sz];
+ for (int i = 0; i < sz; i++) {
+ values[i] = new StatsDimensionsValue(parcel);
+ }
+ return values;
+ }
+ default:
+ Slog.w(TAG, "readValue of an impossible type " + valueType);
+ return null;
+ }
+ }
+}
diff --git a/android/os/SystemProperties.java b/android/os/SystemProperties.java
index 4f6d322..a9b8675 100644
--- a/android/os/SystemProperties.java
+++ b/android/os/SystemProperties.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SystemApi;
import android.util.Log;
import android.util.MutableInt;
@@ -33,6 +34,7 @@
*
* {@hide}
*/
+@SystemApi
public class SystemProperties {
private static final String TAG = "SystemProperties";
private static final boolean TRACK_KEY_ACCESS = false;
@@ -40,9 +42,11 @@
/**
* Android O removed the property name length limit, but com.amazon.kindle 7.8.1.5
* uses reflection to read this whenever text is selected (http://b/36095274).
+ * @hide
*/
public static final int PROP_NAME_MAX = Integer.MAX_VALUE;
+ /** @hide */
public static final int PROP_VALUE_MAX = 91;
@GuardedBy("sChangeCallbacks")
@@ -86,8 +90,10 @@
*
* @param key the key to lookup
* @return an empty string if the {@code key} isn't found
+ * @hide
*/
@NonNull
+ @SystemApi
public static String get(@NonNull String key) {
if (TRACK_KEY_ACCESS) onKeyAccess(key);
return native_get(key);
@@ -100,8 +106,10 @@
* @param def the default value in case the property is not set or empty
* @return if the {@code key} isn't found, return {@code def} if it isn't null, or an empty
* string otherwise
+ * @hide
*/
@NonNull
+ @SystemApi
public static String get(@NonNull String key, @Nullable String def) {
if (TRACK_KEY_ACCESS) onKeyAccess(key);
return native_get(key, def);
@@ -114,7 +122,9 @@
* @param def a default value to return
* @return the key parsed as an integer, or def if the key isn't found or
* cannot be parsed
+ * @hide
*/
+ @SystemApi
public static int getInt(@NonNull String key, int def) {
if (TRACK_KEY_ACCESS) onKeyAccess(key);
return native_get_int(key, def);
@@ -127,7 +137,9 @@
* @param def a default value to return
* @return the key parsed as a long, or def if the key isn't found or
* cannot be parsed
+ * @hide
*/
+ @SystemApi
public static long getLong(@NonNull String key, long def) {
if (TRACK_KEY_ACCESS) onKeyAccess(key);
return native_get_long(key, def);
@@ -145,7 +157,9 @@
* @param def a default value to return
* @return the key parsed as a boolean, or def if the key isn't found or is
* not able to be parsed as a boolean.
+ * @hide
*/
+ @SystemApi
public static boolean getBoolean(@NonNull String key, boolean def) {
if (TRACK_KEY_ACCESS) onKeyAccess(key);
return native_get_boolean(key, def);
@@ -155,6 +169,7 @@
* Set the value for the given {@code key} to {@code val}.
*
* @throws IllegalArgumentException if the {@code val} exceeds 91 characters
+ * @hide
*/
public static void set(@NonNull String key, @Nullable String val) {
if (val != null && !val.startsWith("ro.") && val.length() > PROP_VALUE_MAX) {
@@ -170,6 +185,7 @@
*
* @param callback The {@link Runnable} that should be executed when a system property
* changes.
+ * @hide
*/
public static void addChangeCallback(@NonNull Runnable callback) {
synchronized (sChangeCallbacks) {
@@ -194,10 +210,14 @@
}
}
- /*
+ /**
* Notifies listeners that a system property has changed
+ * @hide
*/
public static void reportSyspropChanged() {
native_report_sysprop_change();
}
+
+ private SystemProperties() {
+ }
}
diff --git a/android/os/SystemUpdateManager.java b/android/os/SystemUpdateManager.java
new file mode 100644
index 0000000..ce3e225
--- /dev/null
+++ b/android/os/SystemUpdateManager.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2018 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.os;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+
+/**
+ * Allows querying and posting system update information.
+ *
+ * {@hide}
+ */
+@SystemApi
+@SystemService(Context.SYSTEM_UPDATE_SERVICE)
+public class SystemUpdateManager {
+ private static final String TAG = "SystemUpdateManager";
+
+ /** The status key of the system update info, expecting an int value. */
+ @SystemApi
+ public static final String KEY_STATUS = "status";
+
+ /** The title of the current update, expecting a String value. */
+ @SystemApi
+ public static final String KEY_TITLE = "title";
+
+ /** Whether it is a security update, expecting a boolean value. */
+ @SystemApi
+ public static final String KEY_IS_SECURITY_UPDATE = "is_security_update";
+
+ /** The build fingerprint after installing the current update, expecting a String value. */
+ @SystemApi
+ public static final String KEY_TARGET_BUILD_FINGERPRINT = "target_build_fingerprint";
+
+ /** The security patch level after installing the current update, expecting a String value. */
+ @SystemApi
+ public static final String KEY_TARGET_SECURITY_PATCH_LEVEL = "target_security_patch_level";
+
+ /**
+ * The KEY_STATUS value that indicates there's no update status info available.
+ */
+ @SystemApi
+ public static final int STATUS_UNKNOWN = 0;
+
+ /**
+ * The KEY_STATUS value that indicates there's no pending update.
+ */
+ @SystemApi
+ public static final int STATUS_IDLE = 1;
+
+ /**
+ * The KEY_STATUS value that indicates an update is available for download, but pending user
+ * approval to start.
+ */
+ @SystemApi
+ public static final int STATUS_WAITING_DOWNLOAD = 2;
+
+ /**
+ * The KEY_STATUS value that indicates an update is in progress (i.e. downloading or installing
+ * has started).
+ */
+ @SystemApi
+ public static final int STATUS_IN_PROGRESS = 3;
+
+ /**
+ * The KEY_STATUS value that indicates an update is available for install.
+ */
+ @SystemApi
+ public static final int STATUS_WAITING_INSTALL = 4;
+
+ /**
+ * The KEY_STATUS value that indicates an update will be installed after a reboot. This applies
+ * to both of A/B and non-A/B OTAs.
+ */
+ @SystemApi
+ public static final int STATUS_WAITING_REBOOT = 5;
+
+ private final ISystemUpdateManager mService;
+
+ /** @hide */
+ public SystemUpdateManager(ISystemUpdateManager service) {
+ mService = checkNotNull(service, "missing ISystemUpdateManager");
+ }
+
+ /**
+ * Queries the current pending system update info.
+ *
+ * <p>Requires the {@link android.Manifest.permission#READ_SYSTEM_UPDATE_INFO} or
+ * {@link android.Manifest.permission#RECOVERY} permission.
+ *
+ * @return A {@code Bundle} that contains the pending system update information in key-value
+ * pairs.
+ *
+ * @throws SecurityException if the caller is not allowed to read the info.
+ */
+ @SystemApi
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.READ_SYSTEM_UPDATE_INFO,
+ android.Manifest.permission.RECOVERY,
+ })
+ public Bundle retrieveSystemUpdateInfo() {
+ try {
+ return mService.retrieveSystemUpdateInfo();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Allows a system updater to publish the pending update info.
+ *
+ * <p>The reported info will not persist across reboots. Because only the reporting updater
+ * understands the criteria to determine a successful/failed update.
+ *
+ * <p>Requires the {@link android.Manifest.permission#RECOVERY} permission.
+ *
+ * @param infoBundle The {@code PersistableBundle} that contains the system update information,
+ * such as the current update status. {@link #KEY_STATUS} is required in the bundle.
+ *
+ * @throws IllegalArgumentException if @link #KEY_STATUS} does not exist.
+ * @throws SecurityException if the caller is not allowed to update the info.
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.RECOVERY)
+ public void updateSystemUpdateInfo(PersistableBundle infoBundle) {
+ if (infoBundle == null || !infoBundle.containsKey(KEY_STATUS)) {
+ throw new IllegalArgumentException("Missing status in the bundle");
+ }
+ try {
+ mService.updateSystemUpdateInfo(infoBundle);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/android/os/UserHandle.java b/android/os/UserHandle.java
index 6381b56..5be72bc 100644
--- a/android/os/UserHandle.java
+++ b/android/os/UserHandle.java
@@ -126,7 +126,10 @@
return getAppId(uid1) == getAppId(uid2);
}
- /** @hide */
+ /**
+ * Whether a UID is an "isolated" UID.
+ * @hide
+ */
public static boolean isIsolated(int uid) {
if (uid > 0) {
final int appId = getAppId(uid);
@@ -136,7 +139,11 @@
}
}
- /** @hide */
+ /**
+ * Whether a UID belongs to a regular app. *Note* "Not a regular app" does not mean
+ * "it's system", because of isolated UIDs. Use {@link #isCore} for that.
+ * @hide
+ */
public static boolean isApp(int uid) {
if (uid > 0) {
final int appId = getAppId(uid);
@@ -147,6 +154,19 @@
}
/**
+ * Whether a UID belongs to a system core component or not.
+ * @hide
+ */
+ public static boolean isCore(int uid) {
+ if (uid > 0) {
+ final int appId = getAppId(uid);
+ return appId < Process.FIRST_APPLICATION_UID;
+ } else {
+ return false;
+ }
+ }
+
+ /**
* Returns the user for a given uid.
* @param uid A uid for an application running in a particular user.
* @return A {@link UserHandle} for that user.
diff --git a/android/os/UserManager.java b/android/os/UserManager.java
index dd9fd93..13b5b5c 100644
--- a/android/os/UserManager.java
+++ b/android/os/UserManager.java
@@ -209,6 +209,49 @@
public static final String DISALLOW_AIRPLANE_MODE = "no_airplane_mode";
/**
+ * Specifies if a user is disallowed from configuring brightness. When device owner sets it,
+ * it'll only be applied on the target(system) user.
+ *
+ * <p>The default value is <code>false</code>.
+ *
+ * <p>This user restriction has no effect on managed profiles.
+ * <p>Key for user restrictions.
+ * <p>Type: Boolean
+ * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
+ * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
+ * @see #getUserRestrictions()
+ */
+ public static final String DISALLOW_CONFIG_BRIGHTNESS = "no_config_brightness";
+
+ /**
+ * Specifies if ambient display is disallowed for the user.
+ *
+ * <p>The default value is <code>false</code>.
+ *
+ * <p>This user restriction has no effect on managed profiles.
+ * <p>Key for user restrictions.
+ * <p>Type: Boolean
+ * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
+ * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
+ * @see #getUserRestrictions()
+ */
+ public static final String DISALLOW_AMBIENT_DISPLAY = "no_ambient_display";
+
+ /**
+ * Specifies if a user is disallowed from changing screen off timeout.
+ *
+ * <p>The default value is <code>false</code>.
+ *
+ * <p>This user restriction has no effect on managed profiles.
+ * <p>Key for user restrictions.
+ * <p>Type: Boolean
+ * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
+ * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
+ * @see #getUserRestrictions()
+ */
+ public static final String DISALLOW_CONFIG_SCREEN_TIMEOUT = "no_config_screen_timeout";
+
+ /**
* Specifies if a user is disallowed from enabling the
* "Unknown Sources" setting, that allows installation of apps from unknown sources.
* The default value is <code>false</code>.
@@ -748,6 +791,7 @@
* @see #getUserRestrictions()
* @hide
*/
+ @SystemApi
public static final String DISALLOW_RUN_IN_BACKGROUND = "no_run_in_background";
/**
@@ -877,6 +921,27 @@
public static final String DISALLOW_USER_SWITCH = "no_user_switch";
/**
+ * Specifies whether the user can share file / picture / data from the primary user into the
+ * managed profile, either by sending them from the primary side, or by picking up data within
+ * an app in the managed profile.
+ * <p>
+ * When a managed profile is created, the system allows the user to send data from the primary
+ * side to the profile by setting up certain default cross profile intent filters. If
+ * this is undesired, this restriction can be set to disallow it. Note that this restriction
+ * will not block any sharing allowed by explicit
+ * {@link DevicePolicyManager#addCrossProfileIntentFilter} calls by the profile owner.
+ * <p>
+ * This restriction is only meaningful when set by profile owner. When it is set by device
+ * owner, it does not have any effect.
+ * <p>
+ * The default value is <code>false</code>.
+ *
+ * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
+ * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
+ * @see #getUserRestrictions()
+ */
+ public static final String DISALLOW_SHARE_INTO_MANAGED_PROFILE = "no_sharing_into_profile";
+ /**
* Application restriction key that is used to indicate the pending arrival
* of real restrictions for the app.
*
@@ -1392,6 +1457,34 @@
}
/**
+ * Return the time when the calling user started in elapsed milliseconds since boot,
+ * or 0 if not started.
+ *
+ * @hide
+ */
+ public long getUserStartRealtime() {
+ try {
+ return mService.getUserStartRealtime();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return the time when the calling user was unlocked elapsed milliseconds since boot,
+ * or 0 if not unlocked.
+ *
+ * @hide
+ */
+ public long getUserUnlockRealtime() {
+ try {
+ return mService.getUserUnlockRealtime();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Returns the UserInfo object describing a specific user.
* Requires {@link android.Manifest.permission#MANAGE_USERS} permission.
* @param userHandle the user handle of the user whose information is being requested.
@@ -2166,6 +2259,12 @@
}
}
+ /** @removed */
+ @Deprecated
+ public boolean trySetQuietModeEnabled(boolean enableQuietMode, @NonNull UserHandle userHandle) {
+ return requestQuietModeEnabled(enableQuietMode, userHandle);
+ }
+
/**
* Enables or disables quiet mode for a managed profile. If quiet mode is enabled, apps in a
* managed profile don't run, generate notifications, or consume data or battery.
@@ -2191,21 +2290,22 @@
*
* @see #isQuietModeEnabled(UserHandle)
*/
- public boolean trySetQuietModeEnabled(boolean enableQuietMode, @NonNull UserHandle userHandle) {
- return trySetQuietModeEnabled(enableQuietMode, userHandle, null);
+ public boolean requestQuietModeEnabled(boolean enableQuietMode, @NonNull UserHandle userHandle) {
+ return requestQuietModeEnabled(enableQuietMode, userHandle, null);
}
/**
- * Similar to {@link #trySetQuietModeEnabled(boolean, UserHandle)}, except you can specify
- * a target to start when user is unlocked.
+ * Similar to {@link #requestQuietModeEnabled(boolean, UserHandle)}, except you can specify
+ * a target to start when user is unlocked. If {@code target} is specified, caller must have
+ * the {@link android.Manifest.permission#MANAGE_USERS} permission.
*
- * @see {@link #trySetQuietModeEnabled(boolean, UserHandle)}
+ * @see {@link #requestQuietModeEnabled(boolean, UserHandle)}
* @hide
*/
- public boolean trySetQuietModeEnabled(
+ public boolean requestQuietModeEnabled(
boolean enableQuietMode, @NonNull UserHandle userHandle, IntentSender target) {
try {
- return mService.trySetQuietModeEnabled(
+ return mService.requestQuietModeEnabled(
mContext.getPackageName(), enableQuietMode, userHandle.getIdentifier(), target);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
diff --git a/android/os/VintfObject.java b/android/os/VintfObject.java
index 340f3fb..12a495b 100644
--- a/android/os/VintfObject.java
+++ b/android/os/VintfObject.java
@@ -76,8 +76,8 @@
/**
* @return a list of VNDK snapshots supported by the framework, as
* specified in framework manifest. For example,
- * [("25.0.5", ["libjpeg.so", "libbase.so"]),
- * ("25.1.3", ["libjpeg.so", "libbase.so"])]
+ * [("27", ["libjpeg.so", "libbase.so"]),
+ * ("28", ["libjpeg.so", "libbase.so"])]
*/
public static native Map<String, String[]> getVndkSnapshots();
}
diff --git a/android/os/WorkSource.java b/android/os/WorkSource.java
index 401b4a3..d0c2870 100644
--- a/android/os/WorkSource.java
+++ b/android/os/WorkSource.java
@@ -7,7 +7,6 @@
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Objects;
/**
* Describes the source of some work that may be done by someone else.
@@ -162,9 +161,21 @@
@Override
public boolean equals(Object o) {
- return o instanceof WorkSource
- && !diff((WorkSource) o)
- && Objects.equals(mChains, ((WorkSource) o).mChains);
+ if (o instanceof WorkSource) {
+ WorkSource other = (WorkSource) o;
+
+ if (diff(other)) {
+ return false;
+ }
+
+ if (mChains != null && !mChains.isEmpty()) {
+ return mChains.equals(other.mChains);
+ } else {
+ return other.mChains == null || other.mChains.isEmpty();
+ }
+ }
+
+ return false;
}
@Override
@@ -407,11 +418,11 @@
}
public boolean remove(WorkSource other) {
- if (mNum <= 0 || other.mNum <= 0) {
+ if (isEmpty() || other.isEmpty()) {
return false;
}
- boolean uidRemoved = false;
+ boolean uidRemoved;
if (mNames == null && other.mNames == null) {
uidRemoved = removeUids(other);
} else {
@@ -427,13 +438,8 @@
}
boolean chainRemoved = false;
- if (other.mChains != null) {
- if (mChains != null) {
- chainRemoved = mChains.removeAll(other.mChains);
- }
- } else if (mChains != null) {
- mChains.clear();
- chainRemoved = true;
+ if (other.mChains != null && mChains != null) {
+ chainRemoved = mChains.removeAll(other.mChains);
}
return uidRemoved || chainRemoved;
diff --git a/android/os/connectivity/GpsBatteryStats.java b/android/os/connectivity/GpsBatteryStats.java
new file mode 100644
index 0000000..f2ac5ef
--- /dev/null
+++ b/android/os/connectivity/GpsBatteryStats.java
@@ -0,0 +1,108 @@
+/*
+ * 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.os.connectivity;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.location.gnssmetrics.GnssMetrics;
+
+import java.util.Arrays;
+
+/**
+ * API for GPS power stats
+ *
+ * @hide
+ */
+public final class GpsBatteryStats implements Parcelable {
+
+ private long mLoggingDurationMs;
+ private long mEnergyConsumedMaMs;
+ private long[] mTimeInGpsSignalQualityLevel;
+
+ public static final Parcelable.Creator<GpsBatteryStats> CREATOR = new
+ Parcelable.Creator<GpsBatteryStats>() {
+ public GpsBatteryStats createFromParcel(Parcel in) {
+ return new GpsBatteryStats(in);
+ }
+
+ public GpsBatteryStats[] newArray(int size) {
+ return new GpsBatteryStats[size];
+ }
+ };
+
+ public GpsBatteryStats() {
+ initialize();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeLong(mLoggingDurationMs);
+ out.writeLong(mEnergyConsumedMaMs);
+ out.writeLongArray(mTimeInGpsSignalQualityLevel);
+ }
+
+ public void readFromParcel(Parcel in) {
+ mLoggingDurationMs = in.readLong();
+ mEnergyConsumedMaMs = in.readLong();
+ in.readLongArray(mTimeInGpsSignalQualityLevel);
+ }
+
+ public long getLoggingDurationMs() {
+ return mLoggingDurationMs;
+ }
+
+ public long getEnergyConsumedMaMs() {
+ return mEnergyConsumedMaMs;
+ }
+
+ public long[] getTimeInGpsSignalQualityLevel() {
+ return mTimeInGpsSignalQualityLevel;
+ }
+
+ public void setLoggingDurationMs(long t) {
+ mLoggingDurationMs = t;
+ return;
+ }
+
+ public void setEnergyConsumedMaMs(long e) {
+ mEnergyConsumedMaMs = e;
+ return;
+ }
+
+ public void setTimeInGpsSignalQualityLevel(long[] t) {
+ mTimeInGpsSignalQualityLevel = Arrays.copyOfRange(t, 0,
+ Math.min(t.length, GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS));
+ return;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ private GpsBatteryStats(Parcel in) {
+ initialize();
+ readFromParcel(in);
+ }
+
+ private void initialize() {
+ mLoggingDurationMs = 0;
+ mEnergyConsumedMaMs = 0;
+ mTimeInGpsSignalQualityLevel = new long[GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS];
+ return;
+ }
+}
\ No newline at end of file
diff --git a/android/os/connectivity/WifiBatteryStats.java b/android/os/connectivity/WifiBatteryStats.java
new file mode 100644
index 0000000..e5341ee
--- /dev/null
+++ b/android/os/connectivity/WifiBatteryStats.java
@@ -0,0 +1,279 @@
+/*
+ * 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.os.connectivity;
+
+import android.os.BatteryStats;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+
+/**
+ * API for Wifi power stats
+ *
+ * @hide
+ */
+public final class WifiBatteryStats implements Parcelable {
+
+ private long mLoggingDurationMs;
+ private long mKernelActiveTimeMs;
+ private long mNumPacketsTx;
+ private long mNumBytesTx;
+ private long mNumPacketsRx;
+ private long mNumBytesRx;
+ private long mSleepTimeMs;
+ private long mScanTimeMs;
+ private long mIdleTimeMs;
+ private long mRxTimeMs;
+ private long mTxTimeMs;
+ private long mEnergyConsumedMaMs;
+ private long mNumAppScanRequest;
+ private long[] mTimeInStateMs;
+ private long[] mTimeInSupplicantStateMs;
+ private long[] mTimeInRxSignalStrengthLevelMs;
+
+ public static final Parcelable.Creator<WifiBatteryStats> CREATOR = new
+ Parcelable.Creator<WifiBatteryStats>() {
+ public WifiBatteryStats createFromParcel(Parcel in) {
+ return new WifiBatteryStats(in);
+ }
+
+ public WifiBatteryStats[] newArray(int size) {
+ return new WifiBatteryStats[size];
+ }
+ };
+
+ public WifiBatteryStats() {
+ initialize();
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeLong(mLoggingDurationMs);
+ out.writeLong(mKernelActiveTimeMs);
+ out.writeLong(mNumPacketsTx);
+ out.writeLong(mNumBytesTx);
+ out.writeLong(mNumPacketsRx);
+ out.writeLong(mNumBytesRx);
+ out.writeLong(mSleepTimeMs);
+ out.writeLong(mScanTimeMs);
+ out.writeLong(mIdleTimeMs);
+ out.writeLong(mRxTimeMs);
+ out.writeLong(mTxTimeMs);
+ out.writeLong(mEnergyConsumedMaMs);
+ out.writeLong(mNumAppScanRequest);
+ out.writeLongArray(mTimeInStateMs);
+ out.writeLongArray(mTimeInRxSignalStrengthLevelMs);
+ out.writeLongArray(mTimeInSupplicantStateMs);
+ }
+
+ public void readFromParcel(Parcel in) {
+ mLoggingDurationMs = in.readLong();
+ mKernelActiveTimeMs = in.readLong();
+ mNumPacketsTx = in.readLong();
+ mNumBytesTx = in.readLong();
+ mNumPacketsRx = in.readLong();
+ mNumBytesRx = in.readLong();
+ mSleepTimeMs = in.readLong();
+ mScanTimeMs = in.readLong();
+ mIdleTimeMs = in.readLong();
+ mRxTimeMs = in.readLong();
+ mTxTimeMs = in.readLong();
+ mEnergyConsumedMaMs = in.readLong();
+ mNumAppScanRequest = in.readLong();
+ in.readLongArray(mTimeInStateMs);
+ in.readLongArray(mTimeInRxSignalStrengthLevelMs);
+ in.readLongArray(mTimeInSupplicantStateMs);
+ }
+
+ public long getLoggingDurationMs() {
+ return mLoggingDurationMs;
+ }
+
+ public long getKernelActiveTimeMs() {
+ return mKernelActiveTimeMs;
+ }
+
+ public long getNumPacketsTx() {
+ return mNumPacketsTx;
+ }
+
+ public long getNumBytesTx() {
+ return mNumBytesTx;
+ }
+
+ public long getNumPacketsRx() {
+ return mNumPacketsRx;
+ }
+
+ public long getNumBytesRx() {
+ return mNumBytesRx;
+ }
+
+ public long getSleepTimeMs() {
+ return mSleepTimeMs;
+ }
+
+ public long getScanTimeMs() {
+ return mScanTimeMs;
+ }
+
+ public long getIdleTimeMs() {
+ return mIdleTimeMs;
+ }
+
+ public long getRxTimeMs() {
+ return mRxTimeMs;
+ }
+
+ public long getTxTimeMs() {
+ return mTxTimeMs;
+ }
+
+ public long getEnergyConsumedMaMs() {
+ return mEnergyConsumedMaMs;
+ }
+
+ public long getNumAppScanRequest() {
+ return mNumAppScanRequest;
+ }
+
+ public long[] getTimeInStateMs() {
+ return mTimeInStateMs;
+ }
+
+ public long[] getTimeInRxSignalStrengthLevelMs() {
+ return mTimeInRxSignalStrengthLevelMs;
+ }
+
+ public long[] getTimeInSupplicantStateMs() {
+ return mTimeInSupplicantStateMs;
+ }
+
+ public void setLoggingDurationMs(long t) {
+ mLoggingDurationMs = t;
+ return;
+ }
+
+ public void setKernelActiveTimeMs(long t) {
+ mKernelActiveTimeMs = t;
+ return;
+ }
+
+ public void setNumPacketsTx(long n) {
+ mNumPacketsTx = n;
+ return;
+ }
+
+ public void setNumBytesTx(long b) {
+ mNumBytesTx = b;
+ return;
+ }
+
+ public void setNumPacketsRx(long n) {
+ mNumPacketsRx = n;
+ return;
+ }
+
+ public void setNumBytesRx(long b) {
+ mNumBytesRx = b;
+ return;
+ }
+
+ public void setSleepTimeMs(long t) {
+ mSleepTimeMs = t;
+ return;
+ }
+
+ public void setScanTimeMs(long t) {
+ mScanTimeMs = t;
+ return;
+ }
+
+ public void setIdleTimeMs(long t) {
+ mIdleTimeMs = t;
+ return;
+ }
+
+ public void setRxTimeMs(long t) {
+ mRxTimeMs = t;
+ return;
+ }
+
+ public void setTxTimeMs(long t) {
+ mTxTimeMs = t;
+ return;
+ }
+
+ public void setEnergyConsumedMaMs(long e) {
+ mEnergyConsumedMaMs = e;
+ return;
+ }
+
+ public void setNumAppScanRequest(long n) {
+ mNumAppScanRequest = n;
+ return;
+ }
+
+ public void setTimeInStateMs(long[] t) {
+ mTimeInStateMs = Arrays.copyOfRange(t, 0,
+ Math.min(t.length, BatteryStats.NUM_WIFI_STATES));
+ return;
+ }
+
+ public void setTimeInRxSignalStrengthLevelMs(long[] t) {
+ mTimeInRxSignalStrengthLevelMs = Arrays.copyOfRange(t, 0,
+ Math.min(t.length, BatteryStats.NUM_WIFI_SIGNAL_STRENGTH_BINS));
+ return;
+ }
+
+ public void setTimeInSupplicantStateMs(long[] t) {
+ mTimeInSupplicantStateMs = Arrays.copyOfRange(
+ t, 0, Math.min(t.length, BatteryStats.NUM_WIFI_SUPPL_STATES));
+ return;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ private WifiBatteryStats(Parcel in) {
+ initialize();
+ readFromParcel(in);
+ }
+
+ private void initialize() {
+ mLoggingDurationMs = 0;
+ mKernelActiveTimeMs = 0;
+ mNumPacketsTx = 0;
+ mNumBytesTx = 0;
+ mNumPacketsRx = 0;
+ mNumBytesRx = 0;
+ mSleepTimeMs = 0;
+ mScanTimeMs = 0;
+ mIdleTimeMs = 0;
+ mRxTimeMs = 0;
+ mTxTimeMs = 0;
+ mEnergyConsumedMaMs = 0;
+ mNumAppScanRequest = 0;
+ mTimeInStateMs = new long[BatteryStats.NUM_WIFI_STATES];
+ Arrays.fill(mTimeInStateMs, 0);
+ mTimeInRxSignalStrengthLevelMs = new long[BatteryStats.NUM_WIFI_SIGNAL_STRENGTH_BINS];
+ Arrays.fill(mTimeInRxSignalStrengthLevelMs, 0);
+ mTimeInSupplicantStateMs = new long[BatteryStats.NUM_WIFI_SUPPL_STATES];
+ Arrays.fill(mTimeInSupplicantStateMs, 0);
+ return;
+ }
+}
\ No newline at end of file
diff --git a/android/os/storage/StorageManager.java b/android/os/storage/StorageManager.java
index 4c587a8..f4deeed 100644
--- a/android/os/storage/StorageManager.java
+++ b/android/os/storage/StorageManager.java
@@ -16,9 +16,6 @@
package android.os.storage;
-import static android.net.TrafficStats.GB_IN_BYTES;
-import static android.net.TrafficStats.MB_IN_BYTES;
-
import android.annotation.BytesLong;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -59,6 +56,7 @@
import android.system.Os;
import android.system.OsConstants;
import android.text.TextUtils;
+import android.util.DataUnit;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
@@ -116,6 +114,8 @@
/** {@hide} */
public static final String PROP_HAS_ADOPTABLE = "vold.has_adoptable";
/** {@hide} */
+ public static final String PROP_HAS_RESERVED = "vold.has_reserved";
+ /** {@hide} */
public static final String PROP_FORCE_ADOPTABLE = "persist.fw.force_adoptable";
/** {@hide} */
public static final String PROP_EMULATE_FBE = "persist.sys.emulate_fbe";
@@ -123,8 +123,6 @@
public static final String PROP_SDCARDFS = "persist.sys.sdcardfs";
/** {@hide} */
public static final String PROP_VIRTUAL_DISK = "persist.sys.virtual_disk";
- /** {@hide} */
- public static final String PROP_ADOPTABLE_FBE = "persist.sys.adoptable_fbe";
/** {@hide} */
public static final String UUID_PRIVATE_INTERNAL = null;
@@ -1199,12 +1197,12 @@
}
private static final int DEFAULT_THRESHOLD_PERCENTAGE = 5;
- private static final long DEFAULT_THRESHOLD_MAX_BYTES = 500 * MB_IN_BYTES;
+ private static final long DEFAULT_THRESHOLD_MAX_BYTES = DataUnit.MEBIBYTES.toBytes(500);
private static final int DEFAULT_CACHE_PERCENTAGE = 10;
- private static final long DEFAULT_CACHE_MAX_BYTES = 5 * GB_IN_BYTES;
+ private static final long DEFAULT_CACHE_MAX_BYTES = DataUnit.GIBIBYTES.toBytes(5);
- private static final long DEFAULT_FULL_THRESHOLD_BYTES = MB_IN_BYTES;
+ private static final long DEFAULT_FULL_THRESHOLD_BYTES = DataUnit.MEBIBYTES.toBytes(1);
/**
* Return the number of available bytes until the given path is considered
@@ -1476,6 +1474,11 @@
}
/** {@hide} */
+ public static boolean hasAdoptable() {
+ return SystemProperties.getBoolean(PROP_HAS_ADOPTABLE, false);
+ }
+
+ /** {@hide} */
public static File maybeTranslateEmulatedPathToInternal(File path) {
// Disabled now that FUSE has been replaced by sdcardfs
return path;
diff --git a/android/os/storage/StorageVolume.java b/android/os/storage/StorageVolume.java
index 070b8c1..839a8bf 100644
--- a/android/os/storage/StorageVolume.java
+++ b/android/os/storage/StorageVolume.java
@@ -394,4 +394,32 @@
parcel.writeString(mFsUuid);
parcel.writeString(mState);
}
+
+ /** {@hide} */
+ public static final class ScopedAccessProviderContract {
+
+ private ScopedAccessProviderContract() {
+ throw new UnsupportedOperationException("contains constants only");
+ }
+
+ public static final String AUTHORITY = "com.android.documentsui.scopedAccess";
+
+ public static final String TABLE_PACKAGES = "packages";
+ public static final String TABLE_PERMISSIONS = "permissions";
+
+ public static final String COL_PACKAGE = "package_name";
+ public static final String COL_VOLUME_UUID = "volume_uuid";
+ public static final String COL_DIRECTORY = "directory";
+ public static final String COL_GRANTED = "granted";
+
+ public static final String[] TABLE_PACKAGES_COLUMNS = new String[] { COL_PACKAGE };
+ public static final String[] TABLE_PERMISSIONS_COLUMNS =
+ new String[] { COL_PACKAGE, COL_VOLUME_UUID, COL_DIRECTORY, COL_GRANTED };
+
+ public static final int TABLE_PACKAGES_COL_PACKAGE = 0;
+ public static final int TABLE_PERMISSIONS_COL_PACKAGE = 0;
+ public static final int TABLE_PERMISSIONS_COL_VOLUME_UUID = 1;
+ public static final int TABLE_PERMISSIONS_COL_DIRECTORY = 2;
+ public static final int TABLE_PERMISSIONS_COL_GRANTED = 3;
+ }
}
diff --git a/android/perftests/utils/BenchmarkState.java b/android/perftests/utils/BenchmarkState.java
index bb9dc4a..da17818 100644
--- a/android/perftests/utils/BenchmarkState.java
+++ b/android/perftests/utils/BenchmarkState.java
@@ -25,7 +25,6 @@
import java.io.File;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
@@ -78,10 +77,7 @@
// Statistics. These values will be filled when the benchmark has finished.
// The computation needs double precision, but long int is fine for final reporting.
- private long mMedian = 0;
- private double mMean = 0.0;
- private double mStandardDeviation = 0.0;
- private long mMin = 0;
+ private Stats mStats;
// Individual duration in nano seconds.
private ArrayList<Long> mResults = new ArrayList<>();
@@ -90,36 +86,6 @@
return TimeUnit.MILLISECONDS.toNanos(ms);
}
- /**
- * Calculates statistics.
- */
- private void calculateSatistics() {
- final int size = mResults.size();
- if (size <= 1) {
- throw new IllegalStateException("At least two results are necessary.");
- }
-
- Collections.sort(mResults);
- mMedian = size % 2 == 0 ? (mResults.get(size / 2) + mResults.get(size / 2 + 1)) / 2 :
- mResults.get(size / 2);
-
- mMin = mResults.get(0);
- for (int i = 0; i < size; ++i) {
- long result = mResults.get(i);
- mMean += result;
- if (result < mMin) {
- mMin = result;
- }
- }
- mMean /= (double) size;
-
- for (int i = 0; i < size; ++i) {
- final double tmp = mResults.get(i) - mMean;
- mStandardDeviation += tmp * tmp;
- }
- mStandardDeviation = Math.sqrt(mStandardDeviation / (double) (size - 1));
- }
-
// Stops the benchmark timer.
// This method can be called only when the timer is running.
public void pauseTiming() {
@@ -173,7 +139,7 @@
if (ENABLE_PROFILING) {
Debug.stopMethodTracing();
}
- calculateSatistics();
+ mStats = new Stats(mResults);
mState = FINISHED;
return false;
}
@@ -224,28 +190,28 @@
if (mState != FINISHED) {
throw new IllegalStateException("The benchmark hasn't finished");
}
- return (long) mMean;
+ return (long) mStats.getMean();
}
private long median() {
if (mState != FINISHED) {
throw new IllegalStateException("The benchmark hasn't finished");
}
- return mMedian;
+ return mStats.getMedian();
}
private long min() {
if (mState != FINISHED) {
throw new IllegalStateException("The benchmark hasn't finished");
}
- return mMin;
+ return mStats.getMin();
}
private long standardDeviation() {
if (mState != FINISHED) {
throw new IllegalStateException("The benchmark hasn't finished");
}
- return (long) mStandardDeviation;
+ return (long) mStats.getStandardDeviation();
}
private String summaryLine() {
diff --git a/android/perftests/utils/ManualBenchmarkState.java b/android/perftests/utils/ManualBenchmarkState.java
new file mode 100644
index 0000000..2c84db1
--- /dev/null
+++ b/android/perftests/utils/ManualBenchmarkState.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2018 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.perftests.utils;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provides a benchmark framework.
+ *
+ * This differs from BenchmarkState in that rather than the class measuring the the elapsed time,
+ * the test passes in the elapsed time.
+ *
+ * Example usage:
+ *
+ * public void sampleMethod() {
+ * ManualBenchmarkState state = new ManualBenchmarkState();
+ *
+ * int[] src = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
+ * long elapsedTime = 0;
+ * while (state.keepRunning(elapsedTime)) {
+ * long startTime = System.nanoTime();
+ * int[] dest = new int[src.length];
+ * System.arraycopy(src, 0, dest, 0, src.length);
+ * elapsedTime = System.nanoTime() - startTime;
+ * }
+ * System.out.println(state.summaryLine());
+ * }
+ *
+ * Or use the PerfManualStatusReporter TestRule.
+ *
+ * Make sure that the overhead of checking the clock does not noticeably affect the results.
+ */
+public final class ManualBenchmarkState {
+ private static final String TAG = ManualBenchmarkState.class.getSimpleName();
+
+ // TODO: Tune these values.
+ // warm-up for duration
+ private static final long WARMUP_DURATION_NS = TimeUnit.SECONDS.toNanos(5);
+ // minimum iterations to warm-up for
+ private static final int WARMUP_MIN_ITERATIONS = 8;
+
+ // target testing for duration
+ private static final long TARGET_TEST_DURATION_NS = TimeUnit.SECONDS.toNanos(16);
+ private static final int MAX_TEST_ITERATIONS = 1000000;
+ private static final int MIN_TEST_ITERATIONS = 10;
+
+ private static final int NOT_STARTED = 0; // The benchmark has not started yet.
+ private static final int WARMUP = 1; // The benchmark is warming up.
+ private static final int RUNNING = 2; // The benchmark is running.
+ private static final int FINISHED = 3; // The benchmark has stopped.
+
+ private int mState = NOT_STARTED; // Current benchmark state.
+
+ private long mWarmupStartTime = 0;
+ private int mWarmupIterations = 0;
+
+ private int mMaxIterations = 0;
+
+ // Individual duration in nano seconds.
+ private ArrayList<Long> mResults = new ArrayList<>();
+
+ // Statistics. These values will be filled when the benchmark has finished.
+ // The computation needs double precision, but long int is fine for final reporting.
+ private Stats mStats;
+
+ private void beginBenchmark(long warmupDuration, int iterations) {
+ mMaxIterations = (int) (TARGET_TEST_DURATION_NS / (warmupDuration / iterations));
+ mMaxIterations = Math.min(MAX_TEST_ITERATIONS,
+ Math.max(mMaxIterations, MIN_TEST_ITERATIONS));
+ mState = RUNNING;
+ }
+
+ /**
+ * Judges whether the benchmark needs more samples.
+ *
+ * For the usage, see class comment.
+ */
+ public boolean keepRunning(long duration) {
+ if (duration < 0) {
+ throw new RuntimeException("duration is negative: " + duration);
+ }
+ switch (mState) {
+ case NOT_STARTED:
+ mState = WARMUP;
+ mWarmupStartTime = System.nanoTime();
+ return true;
+ case WARMUP: {
+ final long timeSinceStartingWarmup = System.nanoTime() - mWarmupStartTime;
+ ++mWarmupIterations;
+ if (mWarmupIterations >= WARMUP_MIN_ITERATIONS
+ && timeSinceStartingWarmup >= WARMUP_DURATION_NS) {
+ beginBenchmark(timeSinceStartingWarmup, mWarmupIterations);
+ }
+ return true;
+ }
+ case RUNNING: {
+ mResults.add(duration);
+ final boolean keepRunning = mResults.size() < mMaxIterations;
+ if (!keepRunning) {
+ mStats = new Stats(mResults);
+ mState = FINISHED;
+ }
+ return keepRunning;
+ }
+ case FINISHED:
+ throw new IllegalStateException("The benchmark has finished.");
+ default:
+ throw new IllegalStateException("The benchmark is in an unknown state.");
+ }
+ }
+
+ private String summaryLine() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("Summary: ");
+ sb.append("median=").append(mStats.getMedian()).append("ns, ");
+ sb.append("mean=").append(mStats.getMean()).append("ns, ");
+ sb.append("min=").append(mStats.getMin()).append("ns, ");
+ sb.append("max=").append(mStats.getMax()).append("ns, ");
+ sb.append("sigma=").append(mStats.getStandardDeviation()).append(", ");
+ sb.append("iteration=").append(mResults.size()).append(", ");
+ sb.append("values=").append(mResults.toString());
+ return sb.toString();
+ }
+
+ public void sendFullStatusReport(Instrumentation instrumentation, String key) {
+ if (mState != FINISHED) {
+ throw new IllegalStateException("The benchmark hasn't finished");
+ }
+ Log.i(TAG, key + summaryLine());
+ final Bundle status = new Bundle();
+ status.putLong(key + "_median", mStats.getMedian());
+ status.putLong(key + "_mean", (long) mStats.getMean());
+ status.putLong(key + "_stddev", (long) mStats.getStandardDeviation());
+ instrumentation.sendStatus(Activity.RESULT_OK, status);
+ }
+}
+
diff --git a/android/perftests/utils/PerfManualStatusReporter.java b/android/perftests/utils/PerfManualStatusReporter.java
new file mode 100644
index 0000000..0de6f1d
--- /dev/null
+++ b/android/perftests/utils/PerfManualStatusReporter.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2018 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.perftests.utils;
+
+import android.support.test.InstrumentationRegistry;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Use this rule to make sure we report the status after the test success.
+ *
+ * <code>
+ *
+ * @Rule public PerfManualStatusReporter mPerfStatusReporter = new PerfManualStatusReporter();
+ * @Test public void functionName() {
+ * ManualBenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ *
+ * int[] src = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
+ * long elapsedTime = 0;
+ * while (state.keepRunning(elapsedTime)) {
+ * long startTime = System.nanoTime();
+ * int[] dest = new int[src.length];
+ * System.arraycopy(src, 0, dest, 0, src.length);
+ * elapsedTime = System.nanoTime() - startTime;
+ * }
+ * }
+ * </code>
+ *
+ * When test succeeded, the status report will use the key as
+ * "functionName_*"
+ */
+
+public class PerfManualStatusReporter implements TestRule {
+ private final ManualBenchmarkState mState;
+
+ public PerfManualStatusReporter() {
+ mState = new ManualBenchmarkState();
+ }
+
+ public ManualBenchmarkState getBenchmarkState() {
+ return mState;
+ }
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ base.evaluate();
+
+ mState.sendFullStatusReport(InstrumentationRegistry.getInstrumentation(),
+ description.getMethodName());
+ }
+ };
+ }
+}
+
diff --git a/android/perftests/utils/Stats.java b/android/perftests/utils/Stats.java
new file mode 100644
index 0000000..acc44a8
--- /dev/null
+++ b/android/perftests/utils/Stats.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2018 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.perftests.utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class Stats {
+ private long mMedian, mMin, mMax;
+ private double mMean, mStandardDeviation;
+
+ /* Calculate stats in constructor. */
+ public Stats(List<Long> values) {
+ // make a copy since we're modifying it
+ values = new ArrayList<>(values);
+ final int size = values.size();
+ if (size < 2) {
+ throw new IllegalArgumentException("At least two results are necessary.");
+ }
+
+ Collections.sort(values);
+
+ mMedian = size % 2 == 0 ? (values.get(size / 2) + values.get(size / 2 - 1)) / 2 :
+ values.get(size / 2);
+
+ mMin = values.get(0);
+ mMax = values.get(values.size() - 1);
+
+ for (int i = 0; i < size; ++i) {
+ long result = values.get(i);
+ mMean += result;
+ }
+ mMean /= (double) size;
+
+ for (int i = 0; i < size; ++i) {
+ final double tmp = values.get(i) - mMean;
+ mStandardDeviation += tmp * tmp;
+ }
+ mStandardDeviation = Math.sqrt(mStandardDeviation / (double) (size - 1));
+ }
+
+ public double getMean() {
+ return mMean;
+ }
+
+ public long getMedian() {
+ return mMedian;
+ }
+
+ public long getMax() {
+ return mMax;
+ }
+
+ public long getMin() {
+ return mMin;
+ }
+
+ public double getStandardDeviation() {
+ return mStandardDeviation;
+ }
+}
diff --git a/android/privacy/internal/rappor/RapporEncoder.java b/android/privacy/internal/rappor/RapporEncoder.java
index 2eca4c9..9ac2b3e 100644
--- a/android/privacy/internal/rappor/RapporEncoder.java
+++ b/android/privacy/internal/rappor/RapporEncoder.java
@@ -33,7 +33,6 @@
public class RapporEncoder implements DifferentialPrivacyEncoder {
// Hard-coded seed and secret for insecure encoder
- private static final long INSECURE_RANDOM_SEED = 0x12345678L;
private static final byte[] INSECURE_SECRET = new byte[]{
(byte) 0xD7, (byte) 0x68, (byte) 0x99, (byte) 0x93,
(byte) 0x94, (byte) 0x13, (byte) 0x53, (byte) 0x54,
@@ -66,8 +65,8 @@
// Use SecureRandom as random generator.
random = sSecureRandom;
} else {
- // Hard-coded random generator, to have deterministic result.
- random = new Random(INSECURE_RANDOM_SEED);
+ // To have deterministic result by hard coding encoder id as seed.
+ random = new Random((long) config.mEncoderId.hashCode());
userSecret = INSECURE_SECRET;
}
mEncoder = new Encoder(random, null, null,
diff --git a/android/provider/AlarmClock.java b/android/provider/AlarmClock.java
index 2169457..7ad9e01 100644
--- a/android/provider/AlarmClock.java
+++ b/android/provider/AlarmClock.java
@@ -154,9 +154,12 @@
public static final String ACTION_SET_TIMER = "android.intent.action.SET_TIMER";
/**
- * Activity Action: Dismiss timers.
+ * Activity Action: Dismiss a timer.
* <p>
- * Dismiss all currently expired timers. If there are no expired timers, then this is a no-op.
+ * The timer to dismiss should be specified using the Intent's data URI, which represents a
+ * deeplink to the timer.
+ * </p><p>
+ * If no data URI is provided, dismiss all expired timers.
* </p>
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
diff --git a/android/provider/CallLog.java b/android/provider/CallLog.java
index 60df467..c6c8d9d 100644
--- a/android/provider/CallLog.java
+++ b/android/provider/CallLog.java
@@ -223,14 +223,13 @@
/** Call was WIFI call. */
public static final int FEATURES_WIFI = 1 << 3;
- /** Call was on RTT at some point */
- public static final int FEATURES_RTT = 1 << 4;
-
/**
* Indicates the call underwent Assisted Dialing.
- * @hide
*/
- public static final Integer FEATURES_ASSISTED_DIALING_USED = 0x10;
+ public static final int FEATURES_ASSISTED_DIALING_USED = 1 << 4;
+
+ /** Call was on RTT at some point */
+ public static final int FEATURES_RTT = 1 << 5;
/**
* The phone number as the user entered it.
diff --git a/android/provider/DocumentsContract.java b/android/provider/DocumentsContract.java
index 99fcdad..e7fd59e 100644
--- a/android/provider/DocumentsContract.java
+++ b/android/provider/DocumentsContract.java
@@ -16,7 +16,6 @@
package android.provider;
-import static android.net.TrafficStats.KB_IN_BYTES;
import static android.system.OsConstants.SEEK_SET;
import static com.android.internal.util.Preconditions.checkArgument;
@@ -51,6 +50,7 @@
import android.os.storage.StorageVolume;
import android.system.ErrnoException;
import android.system.Os;
+import android.util.DataUnit;
import android.util.Log;
import libcore.io.IoUtils;
@@ -173,7 +173,7 @@
/**
* Buffer is large enough to rewind past any EXIF headers.
*/
- private static final int THUMBNAIL_BUFFER_SIZE = (int) (128 * KB_IN_BYTES);
+ private static final int THUMBNAIL_BUFFER_SIZE = (int) DataUnit.KIBIBYTES.toBytes(128);
/** {@hide} */
public static final String EXTERNAL_STORAGE_PROVIDER_AUTHORITY =
diff --git a/android/provider/Settings.java b/android/provider/Settings.java
index 2f86514..1ea4861 100644
--- a/android/provider/Settings.java
+++ b/android/provider/Settings.java
@@ -16,6 +16,16 @@
package android.provider;
+import static android.provider.SettingsValidators.ANY_INTEGER_VALIDATOR;
+import static android.provider.SettingsValidators.ANY_STRING_VALIDATOR;
+import static android.provider.SettingsValidators.BOOLEAN_VALIDATOR;
+import static android.provider.SettingsValidators.COMPONENT_NAME_VALIDATOR;
+import static android.provider.SettingsValidators.LENIENT_IP_ADDRESS_VALIDATOR;
+import static android.provider.SettingsValidators.LOCALE_VALIDATOR;
+import static android.provider.SettingsValidators.NON_NEGATIVE_INTEGER_VALIDATOR;
+import static android.provider.SettingsValidators.PACKAGE_NAME_VALIDATOR;
+import static android.provider.SettingsValidators.URI_VALIDATOR;
+
import android.Manifest;
import android.annotation.IntDef;
import android.annotation.IntRange;
@@ -31,7 +41,6 @@
import android.app.AppOpsManager;
import android.app.Application;
import android.app.NotificationChannel;
-import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
import android.app.SearchManager;
import android.app.WallpaperManager;
@@ -65,6 +74,7 @@
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.UserHandle;
+import android.provider.SettingsValidators.Validator;
import android.speech.tts.TextToSpeech;
import android.telephony.SubscriptionManager;
import android.text.TextUtils;
@@ -76,7 +86,6 @@
import android.util.StatsLog;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.ArrayUtils;
import com.android.internal.widget.ILockSettings;
import java.io.IOException;
@@ -1334,18 +1343,6 @@
= "android.settings.CHANNEL_NOTIFICATION_SETTINGS";
/**
- * Activity Action: Show notification settings for a single {@link NotificationChannelGroup}.
- * <p>
- * Input: {@link #EXTRA_APP_PACKAGE}, the package containing the channel group to display.
- * Input: {@link #EXTRA_CHANNEL_GROUP_ID}, the id of the channel group to display.
- * <p>
- * Output: Nothing.
- */
- @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
- public static final String ACTION_CHANNEL_GROUP_NOTIFICATION_SETTINGS =
- "android.settings.CHANNEL_GROUP_NOTIFICATION_SETTINGS";
-
- /**
* Activity Extra: The package owner of the notification channel settings to display.
* <p>
* This must be passed as an extra field to the {@link #ACTION_CHANNEL_NOTIFICATION_SETTINGS}.
@@ -1361,15 +1358,6 @@
public static final String EXTRA_CHANNEL_ID = "android.provider.extra.CHANNEL_ID";
/**
- * Activity Extra: The {@link NotificationChannelGroup#getId()} of the notification channel
- * group settings to display.
- * <p>
- * This must be passed as an extra field to the
- * {@link #ACTION_CHANNEL_GROUP_NOTIFICATION_SETTINGS}.
- */
- public static final String EXTRA_CHANNEL_GROUP_ID = "android.provider.extra.CHANNEL_GROUP_ID";
-
- /**
* Activity Action: Show notification redaction settings.
*
* @hide
@@ -1491,6 +1479,21 @@
public static final String ACTION_REQUEST_SET_AUTOFILL_SERVICE =
"android.settings.REQUEST_SET_AUTOFILL_SERVICE";
+ /**
+ * Activity Action: Show screen for controlling which apps have access on volume directories.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ * <p>
+ * Applications typically use this action to ask the user to revert the "Do not ask again"
+ * status of directory access requests made by
+ * {@link android.os.storage.StorageVolume#createAccessIntent(String)}.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_STORAGE_VOLUME_ACCESS_SETTINGS =
+ "android.settings.STORAGE_VOLUME_ACCESS_SETTINGS";
+
// End of Intent actions for Settings
/**
@@ -2119,11 +2122,6 @@
private static final float DEFAULT_FONT_SCALE = 1.0f;
- /** @hide */
- public static interface Validator {
- public boolean validate(String value);
- }
-
/**
* The content:// style URL for this table
*/
@@ -2228,41 +2226,6 @@
MOVED_TO_GLOBAL.add(Settings.Global.CERT_PIN_UPDATE_METADATA_URL);
}
- private static final Validator sBooleanValidator =
- new DiscreteValueValidator(new String[] {"0", "1"});
-
- private static final Validator sNonNegativeIntegerValidator = new Validator() {
- @Override
- public boolean validate(String value) {
- try {
- return Integer.parseInt(value) >= 0;
- } catch (NumberFormatException e) {
- return false;
- }
- }
- };
-
- private static final Validator sUriValidator = new Validator() {
- @Override
- public boolean validate(String value) {
- try {
- Uri.decode(value);
- return true;
- } catch (IllegalArgumentException e) {
- return false;
- }
- }
- };
-
- private static final Validator sLenientIpAddressValidator = new Validator() {
- private static final int MAX_IPV6_LENGTH = 45;
-
- @Override
- public boolean validate(String value) {
- return value.length() <= MAX_IPV6_LENGTH;
- }
- };
-
/** @hide */
public static void getMovedToGlobalSettings(Set<String> outKeySet) {
outKeySet.addAll(MOVED_TO_GLOBAL);
@@ -2730,65 +2693,36 @@
putIntForUser(cr, SHOW_GTALK_SERVICE_STATUS, flag ? 1 : 0, userHandle);
}
- private static final class DiscreteValueValidator implements Validator {
- private final String[] mValues;
-
- public DiscreteValueValidator(String[] values) {
- mValues = values;
- }
-
- @Override
- public boolean validate(String value) {
- return ArrayUtils.contains(mValues, value);
- }
- }
-
- private static final class InclusiveIntegerRangeValidator implements Validator {
- private final int mMin;
- private final int mMax;
-
- public InclusiveIntegerRangeValidator(int min, int max) {
- mMin = min;
- mMax = max;
- }
-
- @Override
- public boolean validate(String value) {
- try {
- final int intValue = Integer.parseInt(value);
- return intValue >= mMin && intValue <= mMax;
- } catch (NumberFormatException e) {
- return false;
- }
- }
- }
-
- private static final class InclusiveFloatRangeValidator implements Validator {
- private final float mMin;
- private final float mMax;
-
- public InclusiveFloatRangeValidator(float min, float max) {
- mMin = min;
- mMax = max;
- }
-
- @Override
- public boolean validate(String value) {
- try {
- final float floatValue = Float.parseFloat(value);
- return floatValue >= mMin && floatValue <= mMax;
- } catch (NumberFormatException e) {
- return false;
- }
- }
- }
-
/**
* @deprecated Use {@link android.provider.Settings.Global#STAY_ON_WHILE_PLUGGED_IN} instead
*/
@Deprecated
public static final String STAY_ON_WHILE_PLUGGED_IN = Global.STAY_ON_WHILE_PLUGGED_IN;
+ private static final Validator STAY_ON_WHILE_PLUGGED_IN_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ try {
+ int val = Integer.parseInt(value);
+ return (val == 0)
+ || (val == BatteryManager.BATTERY_PLUGGED_AC)
+ || (val == BatteryManager.BATTERY_PLUGGED_USB)
+ || (val == BatteryManager.BATTERY_PLUGGED_WIRELESS)
+ || (val == (BatteryManager.BATTERY_PLUGGED_AC
+ | BatteryManager.BATTERY_PLUGGED_USB))
+ || (val == (BatteryManager.BATTERY_PLUGGED_AC
+ | BatteryManager.BATTERY_PLUGGED_WIRELESS))
+ || (val == (BatteryManager.BATTERY_PLUGGED_USB
+ | BatteryManager.BATTERY_PLUGGED_WIRELESS))
+ || (val == (BatteryManager.BATTERY_PLUGGED_AC
+ | BatteryManager.BATTERY_PLUGGED_USB
+ | BatteryManager.BATTERY_PLUGGED_WIRELESS));
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+ };
+
/**
* What happens when the user presses the end call button if they're not
* on a call.<br/>
@@ -2802,7 +2736,7 @@
public static final String END_BUTTON_BEHAVIOR = "end_button_behavior";
private static final Validator END_BUTTON_BEHAVIOR_VALIDATOR =
- new InclusiveIntegerRangeValidator(0, 3);
+ new SettingsValidators.InclusiveIntegerRangeValidator(0, 3);
/**
* END_BUTTON_BEHAVIOR value for "go home".
@@ -2828,7 +2762,7 @@
*/
public static final String ADVANCED_SETTINGS = "advanced_settings";
- private static final Validator ADVANCED_SETTINGS_VALIDATOR = sBooleanValidator;
+ private static final Validator ADVANCED_SETTINGS_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* ADVANCED_SETTINGS default value.
@@ -2929,7 +2863,7 @@
@Deprecated
public static final String WIFI_USE_STATIC_IP = "wifi_use_static_ip";
- private static final Validator WIFI_USE_STATIC_IP_VALIDATOR = sBooleanValidator;
+ private static final Validator WIFI_USE_STATIC_IP_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* The static IP address.
@@ -2941,7 +2875,7 @@
@Deprecated
public static final String WIFI_STATIC_IP = "wifi_static_ip";
- private static final Validator WIFI_STATIC_IP_VALIDATOR = sLenientIpAddressValidator;
+ private static final Validator WIFI_STATIC_IP_VALIDATOR = LENIENT_IP_ADDRESS_VALIDATOR;
/**
* If using static IP, the gateway's IP address.
@@ -2953,7 +2887,7 @@
@Deprecated
public static final String WIFI_STATIC_GATEWAY = "wifi_static_gateway";
- private static final Validator WIFI_STATIC_GATEWAY_VALIDATOR = sLenientIpAddressValidator;
+ private static final Validator WIFI_STATIC_GATEWAY_VALIDATOR = LENIENT_IP_ADDRESS_VALIDATOR;
/**
* If using static IP, the net mask.
@@ -2965,7 +2899,7 @@
@Deprecated
public static final String WIFI_STATIC_NETMASK = "wifi_static_netmask";
- private static final Validator WIFI_STATIC_NETMASK_VALIDATOR = sLenientIpAddressValidator;
+ private static final Validator WIFI_STATIC_NETMASK_VALIDATOR = LENIENT_IP_ADDRESS_VALIDATOR;
/**
* If using static IP, the primary DNS's IP address.
@@ -2977,7 +2911,7 @@
@Deprecated
public static final String WIFI_STATIC_DNS1 = "wifi_static_dns1";
- private static final Validator WIFI_STATIC_DNS1_VALIDATOR = sLenientIpAddressValidator;
+ private static final Validator WIFI_STATIC_DNS1_VALIDATOR = LENIENT_IP_ADDRESS_VALIDATOR;
/**
* If using static IP, the secondary DNS's IP address.
@@ -2989,7 +2923,7 @@
@Deprecated
public static final String WIFI_STATIC_DNS2 = "wifi_static_dns2";
- private static final Validator WIFI_STATIC_DNS2_VALIDATOR = sLenientIpAddressValidator;
+ private static final Validator WIFI_STATIC_DNS2_VALIDATOR = LENIENT_IP_ADDRESS_VALIDATOR;
/**
* Determines whether remote devices may discover and/or connect to
@@ -3003,7 +2937,7 @@
"bluetooth_discoverability";
private static final Validator BLUETOOTH_DISCOVERABILITY_VALIDATOR =
- new InclusiveIntegerRangeValidator(0, 2);
+ new SettingsValidators.InclusiveIntegerRangeValidator(0, 2);
/**
* Bluetooth discoverability timeout. If this value is nonzero, then
@@ -3014,7 +2948,7 @@
"bluetooth_discoverability_timeout";
private static final Validator BLUETOOTH_DISCOVERABILITY_TIMEOUT_VALIDATOR =
- sNonNegativeIntegerValidator;
+ NON_NEGATIVE_INTEGER_VALIDATOR;
/**
* @deprecated Use {@link android.provider.Settings.Secure#LOCK_PATTERN_ENABLED}
@@ -3110,7 +3044,7 @@
@Deprecated
public static final String DIM_SCREEN = "dim_screen";
- private static final Validator DIM_SCREEN_VALIDATOR = sBooleanValidator;
+ private static final Validator DIM_SCREEN_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* The display color mode.
@@ -3130,7 +3064,8 @@
*/
public static final String SCREEN_OFF_TIMEOUT = "screen_off_timeout";
- private static final Validator SCREEN_OFF_TIMEOUT_VALIDATOR = sNonNegativeIntegerValidator;
+ private static final Validator SCREEN_OFF_TIMEOUT_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
/**
* The screen backlight brightness between 0 and 255.
@@ -3138,7 +3073,7 @@
public static final String SCREEN_BRIGHTNESS = "screen_brightness";
private static final Validator SCREEN_BRIGHTNESS_VALIDATOR =
- new InclusiveIntegerRangeValidator(0, 255);
+ new SettingsValidators.InclusiveIntegerRangeValidator(0, 255);
/**
* The screen backlight brightness between 0 and 255.
@@ -3147,14 +3082,14 @@
public static final String SCREEN_BRIGHTNESS_FOR_VR = "screen_brightness_for_vr";
private static final Validator SCREEN_BRIGHTNESS_FOR_VR_VALIDATOR =
- new InclusiveIntegerRangeValidator(0, 255);
+ new SettingsValidators.InclusiveIntegerRangeValidator(0, 255);
/**
* Control whether to enable automatic brightness mode.
*/
public static final String SCREEN_BRIGHTNESS_MODE = "screen_brightness_mode";
- private static final Validator SCREEN_BRIGHTNESS_MODE_VALIDATOR = sBooleanValidator;
+ private static final Validator SCREEN_BRIGHTNESS_MODE_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Adjustment to auto-brightness to make it generally more (>0.0 <1.0)
@@ -3164,7 +3099,7 @@
public static final String SCREEN_AUTO_BRIGHTNESS_ADJ = "screen_auto_brightness_adj";
private static final Validator SCREEN_AUTO_BRIGHTNESS_ADJ_VALIDATOR =
- new InclusiveFloatRangeValidator(-1, 1);
+ new SettingsValidators.InclusiveFloatRangeValidator(-1, 1);
/**
* SCREEN_BRIGHTNESS_MODE value for manual mode.
@@ -3203,7 +3138,7 @@
public static final String MODE_RINGER_STREAMS_AFFECTED = "mode_ringer_streams_affected";
private static final Validator MODE_RINGER_STREAMS_AFFECTED_VALIDATOR =
- sNonNegativeIntegerValidator;
+ NON_NEGATIVE_INTEGER_VALIDATOR;
/**
* Determines which streams are affected by mute. The
@@ -3213,7 +3148,7 @@
public static final String MUTE_STREAMS_AFFECTED = "mute_streams_affected";
private static final Validator MUTE_STREAMS_AFFECTED_VALIDATOR =
- sNonNegativeIntegerValidator;
+ NON_NEGATIVE_INTEGER_VALIDATOR;
/**
* Whether vibrate is on for different events. This is used internally,
@@ -3221,7 +3156,7 @@
*/
public static final String VIBRATE_ON = "vibrate_on";
- private static final Validator VIBRATE_ON_VALIDATOR = sBooleanValidator;
+ private static final Validator VIBRATE_ON_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* If 1, redirects the system vibrator to all currently attached input devices
@@ -3237,7 +3172,7 @@
*/
public static final String VIBRATE_INPUT_DEVICES = "vibrate_input_devices";
- private static final Validator VIBRATE_INPUT_DEVICES_VALIDATOR = sBooleanValidator;
+ private static final Validator VIBRATE_INPUT_DEVICES_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Ringer volume. This is used internally, changing this value will not
@@ -3316,7 +3251,7 @@
*/
public static final String MASTER_MONO = "master_mono";
- private static final Validator MASTER_MONO_VALIDATOR = sBooleanValidator;
+ private static final Validator MASTER_MONO_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Whether the notifications should use the ring volume (value of 1) or
@@ -3336,7 +3271,7 @@
public static final String NOTIFICATIONS_USE_RING_VOLUME =
"notifications_use_ring_volume";
- private static final Validator NOTIFICATIONS_USE_RING_VOLUME_VALIDATOR = sBooleanValidator;
+ private static final Validator NOTIFICATIONS_USE_RING_VOLUME_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Whether silent mode should allow vibration feedback. This is used
@@ -3352,7 +3287,7 @@
*/
public static final String VIBRATE_IN_SILENT = "vibrate_in_silent";
- private static final Validator VIBRATE_IN_SILENT_VALIDATOR = sBooleanValidator;
+ private static final Validator VIBRATE_IN_SILENT_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* The mapping of stream type (integer) to its setting.
@@ -3400,7 +3335,7 @@
*/
public static final String RINGTONE = "ringtone";
- private static final Validator RINGTONE_VALIDATOR = sUriValidator;
+ private static final Validator RINGTONE_VALIDATOR = URI_VALIDATOR;
/**
* A {@link Uri} that will point to the current default ringtone at any
@@ -3425,7 +3360,7 @@
*/
public static final String NOTIFICATION_SOUND = "notification_sound";
- private static final Validator NOTIFICATION_SOUND_VALIDATOR = sUriValidator;
+ private static final Validator NOTIFICATION_SOUND_VALIDATOR = URI_VALIDATOR;
/**
* A {@link Uri} that will point to the current default notification
@@ -3448,7 +3383,7 @@
*/
public static final String ALARM_ALERT = "alarm_alert";
- private static final Validator ALARM_ALERT_VALIDATOR = sUriValidator;
+ private static final Validator ALARM_ALERT_VALIDATOR = URI_VALIDATOR;
/**
* A {@link Uri} that will point to the current default alarm alert at
@@ -3470,31 +3405,21 @@
*/
public static final String MEDIA_BUTTON_RECEIVER = "media_button_receiver";
- private static final Validator MEDIA_BUTTON_RECEIVER_VALIDATOR = new Validator() {
- @Override
- public boolean validate(String value) {
- try {
- ComponentName.unflattenFromString(value);
- return true;
- } catch (NullPointerException e) {
- return false;
- }
- }
- };
+ private static final Validator MEDIA_BUTTON_RECEIVER_VALIDATOR = COMPONENT_NAME_VALIDATOR;
/**
* Setting to enable Auto Replace (AutoText) in text editors. 1 = On, 0 = Off
*/
public static final String TEXT_AUTO_REPLACE = "auto_replace";
- private static final Validator TEXT_AUTO_REPLACE_VALIDATOR = sBooleanValidator;
+ private static final Validator TEXT_AUTO_REPLACE_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Setting to enable Auto Caps in text editors. 1 = On, 0 = Off
*/
public static final String TEXT_AUTO_CAPS = "auto_caps";
- private static final Validator TEXT_AUTO_CAPS_VALIDATOR = sBooleanValidator;
+ private static final Validator TEXT_AUTO_CAPS_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Setting to enable Auto Punctuate in text editors. 1 = On, 0 = Off. This
@@ -3502,19 +3427,19 @@
*/
public static final String TEXT_AUTO_PUNCTUATE = "auto_punctuate";
- private static final Validator TEXT_AUTO_PUNCTUATE_VALIDATOR = sBooleanValidator;
+ private static final Validator TEXT_AUTO_PUNCTUATE_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Setting to showing password characters in text editors. 1 = On, 0 = Off
*/
public static final String TEXT_SHOW_PASSWORD = "show_password";
- private static final Validator TEXT_SHOW_PASSWORD_VALIDATOR = sBooleanValidator;
+ private static final Validator TEXT_SHOW_PASSWORD_VALIDATOR = BOOLEAN_VALIDATOR;
public static final String SHOW_GTALK_SERVICE_STATUS =
"SHOW_GTALK_SERVICE_STATUS";
- private static final Validator SHOW_GTALK_SERVICE_STATUS_VALIDATOR = sBooleanValidator;
+ private static final Validator SHOW_GTALK_SERVICE_STATUS_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Name of activity to use for wallpaper on the home screen.
@@ -3543,6 +3468,8 @@
@Deprecated
public static final String AUTO_TIME = Global.AUTO_TIME;
+ private static final Validator AUTO_TIME_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#AUTO_TIME_ZONE}
* instead
@@ -3550,6 +3477,8 @@
@Deprecated
public static final String AUTO_TIME_ZONE = Global.AUTO_TIME_ZONE;
+ private static final Validator AUTO_TIME_ZONE_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Display times as 12 or 24 hours
* 12
@@ -3559,7 +3488,7 @@
/** @hide */
public static final Validator TIME_12_24_VALIDATOR =
- new DiscreteValueValidator(new String[] {"12", "24", null});
+ new SettingsValidators.DiscreteValueValidator(new String[] {"12", "24", null});
/**
* Date format string
@@ -3592,7 +3521,7 @@
public static final String SETUP_WIZARD_HAS_RUN = "setup_wizard_has_run";
/** @hide */
- public static final Validator SETUP_WIZARD_HAS_RUN_VALIDATOR = sBooleanValidator;
+ public static final Validator SETUP_WIZARD_HAS_RUN_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Scaling factor for normal window animations. Setting to 0 will disable window
@@ -3631,7 +3560,7 @@
public static final String ACCELEROMETER_ROTATION = "accelerometer_rotation";
/** @hide */
- public static final Validator ACCELEROMETER_ROTATION_VALIDATOR = sBooleanValidator;
+ public static final Validator ACCELEROMETER_ROTATION_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Default screen rotation when no other policy applies.
@@ -3645,7 +3574,7 @@
/** @hide */
public static final Validator USER_ROTATION_VALIDATOR =
- new InclusiveIntegerRangeValidator(0, 3);
+ new SettingsValidators.InclusiveIntegerRangeValidator(0, 3);
/**
* Control whether the rotation lock toggle in the System UI should be hidden.
@@ -3663,7 +3592,7 @@
/** @hide */
public static final Validator HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY_VALIDATOR =
- sBooleanValidator;
+ BOOLEAN_VALIDATOR;
/**
* Whether the phone vibrates when it is ringing due to an incoming call. This will
@@ -3678,7 +3607,7 @@
public static final String VIBRATE_WHEN_RINGING = "vibrate_when_ringing";
/** @hide */
- public static final Validator VIBRATE_WHEN_RINGING_VALIDATOR = sBooleanValidator;
+ public static final Validator VIBRATE_WHEN_RINGING_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Whether the audible DTMF tones are played by the dialer when dialing. The value is
@@ -3687,7 +3616,7 @@
public static final String DTMF_TONE_WHEN_DIALING = "dtmf_tone";
/** @hide */
- public static final Validator DTMF_TONE_WHEN_DIALING_VALIDATOR = sBooleanValidator;
+ public static final Validator DTMF_TONE_WHEN_DIALING_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* CDMA only settings
@@ -3698,7 +3627,7 @@
public static final String DTMF_TONE_TYPE_WHEN_DIALING = "dtmf_tone_type";
/** @hide */
- public static final Validator DTMF_TONE_TYPE_WHEN_DIALING_VALIDATOR = sBooleanValidator;
+ public static final Validator DTMF_TONE_TYPE_WHEN_DIALING_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Whether the hearing aid is enabled. The value is
@@ -3708,7 +3637,7 @@
public static final String HEARING_AID = "hearing_aid";
/** @hide */
- public static final Validator HEARING_AID_VALIDATOR = sBooleanValidator;
+ public static final Validator HEARING_AID_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* CDMA only settings
@@ -3722,7 +3651,8 @@
public static final String TTY_MODE = "tty_mode";
/** @hide */
- public static final Validator TTY_MODE_VALIDATOR = new InclusiveIntegerRangeValidator(0, 3);
+ public static final Validator TTY_MODE_VALIDATOR =
+ new SettingsValidators.InclusiveIntegerRangeValidator(0, 3);
/**
* Whether the sounds effects (key clicks, lid open ...) are enabled. The value is
@@ -3731,7 +3661,7 @@
public static final String SOUND_EFFECTS_ENABLED = "sound_effects_enabled";
/** @hide */
- public static final Validator SOUND_EFFECTS_ENABLED_VALIDATOR = sBooleanValidator;
+ public static final Validator SOUND_EFFECTS_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Whether the haptic feedback (long presses, ...) are enabled. The value is
@@ -3740,7 +3670,7 @@
public static final String HAPTIC_FEEDBACK_ENABLED = "haptic_feedback_enabled";
/** @hide */
- public static final Validator HAPTIC_FEEDBACK_ENABLED_VALIDATOR = sBooleanValidator;
+ public static final Validator HAPTIC_FEEDBACK_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* @deprecated Each application that shows web suggestions should have its own
@@ -3750,7 +3680,7 @@
public static final String SHOW_WEB_SUGGESTIONS = "show_web_suggestions";
/** @hide */
- public static final Validator SHOW_WEB_SUGGESTIONS_VALIDATOR = sBooleanValidator;
+ public static final Validator SHOW_WEB_SUGGESTIONS_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Whether the notification LED should repeatedly flash when a notification is
@@ -3760,7 +3690,7 @@
public static final String NOTIFICATION_LIGHT_PULSE = "notification_light_pulse";
/** @hide */
- public static final Validator NOTIFICATION_LIGHT_PULSE_VALIDATOR = sBooleanValidator;
+ public static final Validator NOTIFICATION_LIGHT_PULSE_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Show pointer location on screen?
@@ -3771,7 +3701,7 @@
public static final String POINTER_LOCATION = "pointer_location";
/** @hide */
- public static final Validator POINTER_LOCATION_VALIDATOR = sBooleanValidator;
+ public static final Validator POINTER_LOCATION_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Show touch positions on screen?
@@ -3782,7 +3712,7 @@
public static final String SHOW_TOUCHES = "show_touches";
/** @hide */
- public static final Validator SHOW_TOUCHES_VALIDATOR = sBooleanValidator;
+ public static final Validator SHOW_TOUCHES_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Log raw orientation data from
@@ -3796,7 +3726,7 @@
"window_orientation_listener_log";
/** @hide */
- public static final Validator WINDOW_ORIENTATION_LISTENER_LOG_VALIDATOR = sBooleanValidator;
+ public static final Validator WINDOW_ORIENTATION_LISTENER_LOG_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* @deprecated Use {@link android.provider.Settings.Global#POWER_SOUNDS_ENABLED}
@@ -3806,6 +3736,8 @@
@Deprecated
public static final String POWER_SOUNDS_ENABLED = Global.POWER_SOUNDS_ENABLED;
+ private static final Validator POWER_SOUNDS_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#DOCK_SOUNDS_ENABLED}
* instead
@@ -3814,6 +3746,8 @@
@Deprecated
public static final String DOCK_SOUNDS_ENABLED = Global.DOCK_SOUNDS_ENABLED;
+ private static final Validator DOCK_SOUNDS_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether to play sounds when the keyguard is shown and dismissed.
* @hide
@@ -3821,7 +3755,7 @@
public static final String LOCKSCREEN_SOUNDS_ENABLED = "lockscreen_sounds_enabled";
/** @hide */
- public static final Validator LOCKSCREEN_SOUNDS_ENABLED_VALIDATOR = sBooleanValidator;
+ public static final Validator LOCKSCREEN_SOUNDS_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Whether the lockscreen should be completely disabled.
@@ -3830,7 +3764,7 @@
public static final String LOCKSCREEN_DISABLED = "lockscreen.disabled";
/** @hide */
- public static final Validator LOCKSCREEN_DISABLED_VALIDATOR = sBooleanValidator;
+ public static final Validator LOCKSCREEN_DISABLED_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* @deprecated Use {@link android.provider.Settings.Global#LOW_BATTERY_SOUND}
@@ -3897,7 +3831,7 @@
public static final String SIP_RECEIVE_CALLS = "sip_receive_calls";
/** @hide */
- public static final Validator SIP_RECEIVE_CALLS_VALIDATOR = sBooleanValidator;
+ public static final Validator SIP_RECEIVE_CALLS_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Call Preference String.
@@ -3908,8 +3842,9 @@
public static final String SIP_CALL_OPTIONS = "sip_call_options";
/** @hide */
- public static final Validator SIP_CALL_OPTIONS_VALIDATOR = new DiscreteValueValidator(
- new String[] {"SIP_ALWAYS", "SIP_ADDRESS_ONLY"});
+ public static final Validator SIP_CALL_OPTIONS_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(
+ new String[] {"SIP_ALWAYS", "SIP_ADDRESS_ONLY"});
/**
* One of the sip call options: Always use SIP with network access.
@@ -3918,7 +3853,7 @@
public static final String SIP_ALWAYS = "SIP_ALWAYS";
/** @hide */
- public static final Validator SIP_ALWAYS_VALIDATOR = sBooleanValidator;
+ public static final Validator SIP_ALWAYS_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* One of the sip call options: Only if destination is a SIP address.
@@ -3927,7 +3862,7 @@
public static final String SIP_ADDRESS_ONLY = "SIP_ADDRESS_ONLY";
/** @hide */
- public static final Validator SIP_ADDRESS_ONLY_VALIDATOR = sBooleanValidator;
+ public static final Validator SIP_ADDRESS_ONLY_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* @deprecated Use SIP_ALWAYS or SIP_ADDRESS_ONLY instead. Formerly used to indicate that
@@ -3940,7 +3875,7 @@
public static final String SIP_ASK_ME_EACH_TIME = "SIP_ASK_ME_EACH_TIME";
/** @hide */
- public static final Validator SIP_ASK_ME_EACH_TIME_VALIDATOR = sBooleanValidator;
+ public static final Validator SIP_ASK_ME_EACH_TIME_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Pointer speed setting.
@@ -3954,7 +3889,7 @@
/** @hide */
public static final Validator POINTER_SPEED_VALIDATOR =
- new InclusiveFloatRangeValidator(-7, 7);
+ new SettingsValidators.InclusiveFloatRangeValidator(-7, 7);
/**
* Whether lock-to-app will be triggered by long-press on recents.
@@ -3963,7 +3898,7 @@
public static final String LOCK_TO_APP_ENABLED = "lock_to_app_enabled";
/** @hide */
- public static final Validator LOCK_TO_APP_ENABLED_VALIDATOR = sBooleanValidator;
+ public static final Validator LOCK_TO_APP_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* I am the lolrus.
@@ -3995,7 +3930,7 @@
public static final String SHOW_BATTERY_PERCENT = "status_bar_show_battery_percent";
/** @hide */
- private static final Validator SHOW_BATTERY_PERCENT_VALIDATOR = sBooleanValidator;
+ private static final Validator SHOW_BATTERY_PERCENT_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* IMPORTANT: If you add a new public settings you also have to add it to
@@ -4067,6 +4002,9 @@
* Keys we no longer back up under the current schema, but want to continue to
* process when restoring historical backup datasets.
*
+ * All settings in {@link LEGACY_RESTORE_SETTINGS} array *must* have a non-null validator,
+ * otherwise they won't be restored.
+ *
* @hide
*/
public static final String[] LEGACY_RESTORE_SETTINGS = {
@@ -4175,11 +4113,15 @@
/**
* These are all public system settings
*
+ * All settings in {@link SETTINGS_TO_BACKUP} array *must* have a non-null validator,
+ * otherwise they won't be restored.
+ *
* @hide
*/
public static final Map<String, Validator> VALIDATORS = new ArrayMap<>();
static {
- VALIDATORS.put(END_BUTTON_BEHAVIOR,END_BUTTON_BEHAVIOR_VALIDATOR);
+ VALIDATORS.put(STAY_ON_WHILE_PLUGGED_IN, STAY_ON_WHILE_PLUGGED_IN_VALIDATOR);
+ VALIDATORS.put(END_BUTTON_BEHAVIOR, END_BUTTON_BEHAVIOR_VALIDATOR);
VALIDATORS.put(WIFI_USE_STATIC_IP, WIFI_USE_STATIC_IP_VALIDATOR);
VALIDATORS.put(BLUETOOTH_DISCOVERABILITY, BLUETOOTH_DISCOVERABILITY_VALIDATOR);
VALIDATORS.put(BLUETOOTH_DISCOVERABILITY_TIMEOUT,
@@ -4201,6 +4143,8 @@
VALIDATORS.put(TEXT_AUTO_CAPS, TEXT_AUTO_CAPS_VALIDATOR);
VALIDATORS.put(TEXT_AUTO_PUNCTUATE, TEXT_AUTO_PUNCTUATE_VALIDATOR);
VALIDATORS.put(TEXT_SHOW_PASSWORD, TEXT_SHOW_PASSWORD_VALIDATOR);
+ VALIDATORS.put(AUTO_TIME, AUTO_TIME_VALIDATOR);
+ VALIDATORS.put(AUTO_TIME_ZONE, AUTO_TIME_ZONE_VALIDATOR);
VALIDATORS.put(SHOW_GTALK_SERVICE_STATUS, SHOW_GTALK_SERVICE_STATUS_VALIDATOR);
VALIDATORS.put(WALLPAPER_ACTIVITY, WALLPAPER_ACTIVITY_VALIDATOR);
VALIDATORS.put(TIME_12_24, TIME_12_24_VALIDATOR);
@@ -4211,6 +4155,8 @@
VALIDATORS.put(DTMF_TONE_WHEN_DIALING, DTMF_TONE_WHEN_DIALING_VALIDATOR);
VALIDATORS.put(SOUND_EFFECTS_ENABLED, SOUND_EFFECTS_ENABLED_VALIDATOR);
VALIDATORS.put(HAPTIC_FEEDBACK_ENABLED, HAPTIC_FEEDBACK_ENABLED_VALIDATOR);
+ VALIDATORS.put(POWER_SOUNDS_ENABLED, POWER_SOUNDS_ENABLED_VALIDATOR);
+ VALIDATORS.put(DOCK_SOUNDS_ENABLED, DOCK_SOUNDS_ENABLED_VALIDATOR);
VALIDATORS.put(SHOW_WEB_SUGGESTIONS, SHOW_WEB_SUGGESTIONS_VALIDATOR);
VALIDATORS.put(WIFI_USE_STATIC_IP, WIFI_USE_STATIC_IP_VALIDATOR);
VALIDATORS.put(END_BUTTON_BEHAVIOR, END_BUTTON_BEHAVIOR_VALIDATOR);
@@ -4335,6 +4281,8 @@
@Deprecated
public static final String BLUETOOTH_ON = Global.BLUETOOTH_ON;
+ private static final Validator BLUETOOTH_ON_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#DATA_ROAMING} instead
*/
@@ -4412,6 +4360,8 @@
@Deprecated
public static final String USB_MASS_STORAGE_ENABLED = Global.USB_MASS_STORAGE_ENABLED;
+ private static final Validator USB_MASS_STORAGE_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#USE_GOOGLE_MAIL} instead
*/
@@ -4441,6 +4391,9 @@
public static final String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON =
Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON;
+ private static final Validator WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use
* {@link android.provider.Settings.Global#WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY} instead
@@ -4449,6 +4402,9 @@
public static final String WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY =
Global.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY;
+ private static final Validator WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#WIFI_NUM_OPEN_NETWORKS_KEPT}
* instead
@@ -4456,6 +4412,9 @@
@Deprecated
public static final String WIFI_NUM_OPEN_NETWORKS_KEPT = Global.WIFI_NUM_OPEN_NETWORKS_KEPT;
+ private static final Validator WIFI_NUM_OPEN_NETWORKS_KEPT_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#WIFI_ON} instead
*/
@@ -5218,6 +5177,8 @@
@Deprecated
public static final String BUGREPORT_IN_POWER_MENU = "bugreport_in_power_menu";
+ private static final Validator BUGREPORT_IN_POWER_MENU_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#ADB_ENABLED} instead
*/
@@ -5235,6 +5196,8 @@
@Deprecated
public static final String ALLOW_MOCK_LOCATION = "mock_location";
+ private static final Validator ALLOW_MOCK_LOCATION_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* On Android 8.0 (API level 26) and higher versions of the platform,
* a 64-bit number (expressed as a hexadecimal string), unique to
@@ -5280,6 +5243,8 @@
@Deprecated
public static final String BLUETOOTH_ON = Global.BLUETOOTH_ON;
+ private static final Validator BLUETOOTH_ON_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#DATA_ROAMING} instead
*/
@@ -5327,6 +5292,8 @@
@TestApi
public static final String AUTOFILL_SERVICE = "autofill_service";
+ private static final Validator AUTOFILL_SERVICE_VALIDATOR = COMPONENT_NAME_VALIDATOR;
+
/**
* Boolean indicating if Autofill supports field classification.
*
@@ -5413,9 +5380,38 @@
* List of input methods that are currently enabled. This is a string
* containing the IDs of all enabled input methods, each ID separated
* by ':'.
+ *
+ * Format like "ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0"
+ * where imeId is ComponentName and subtype is int32.
*/
public static final String ENABLED_INPUT_METHODS = "enabled_input_methods";
+ private static final Validator ENABLED_INPUT_METHODS_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ if (value == null) {
+ return false;
+ }
+ String[] inputMethods = value.split(":");
+ boolean valid = true;
+ for (String inputMethod : inputMethods) {
+ if (inputMethod.length() == 0) {
+ return false;
+ }
+ String[] subparts = inputMethod.split(";");
+ for (String subpart : subparts) {
+ // allow either a non negative integer or a ComponentName
+ valid |= (NON_NEGATIVE_INTEGER_VALIDATOR.validate(subpart)
+ || COMPONENT_NAME_VALIDATOR.validate(subpart));
+ }
+ if (!valid) {
+ return false;
+ }
+ }
+ return valid;
+ }
+ };
+
/**
* List of system input methods that are currently disabled. This is a string
* containing the IDs of all disabled input methods, each ID separated
@@ -5431,6 +5427,8 @@
*/
public static final String SHOW_IME_WITH_HARD_KEYBOARD = "show_ime_with_hard_keyboard";
+ private static final Validator SHOW_IME_WITH_HARD_KEYBOARD_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Host name and port for global http proxy. Uses ':' seperator for
* between host and port.
@@ -5503,37 +5501,54 @@
* Note: do not rely on this value being present in settings.db or on ContentObserver
* notifications for the corresponding Uri. Use {@link LocationManager#MODE_CHANGED_ACTION}
* to receive changes in this value.
+ *
+ * @deprecated To check location status, use {@link LocationManager#isLocationEnabled()}. To
+ * get the status of a location provider, use
+ * {@link LocationManager#isProviderEnabled(String)}.
*/
+ @Deprecated
public static final String LOCATION_MODE = "location_mode";
- /**
- * Stores the previous location mode when {@link #LOCATION_MODE} is set to
- * {@link #LOCATION_MODE_OFF}
- * @hide
- */
- public static final String LOCATION_PREVIOUS_MODE = "location_previous_mode";
/**
- * Sets all location providers to the previous states before location was turned off.
- * @hide
- */
- public static final int LOCATION_MODE_PREVIOUS = -1;
- /**
* Location access disabled.
+ *
+ * @deprecated To check location status, use {@link LocationManager#isLocationEnabled()}. To
+ * get the status of a location provider, use
+ * {@link LocationManager#isProviderEnabled(String)}.
*/
+ @Deprecated
public static final int LOCATION_MODE_OFF = 0;
+
/**
* Network Location Provider disabled, but GPS and other sensors enabled.
+ *
+ * @deprecated To check location status, use {@link LocationManager#isLocationEnabled()}. To
+ * get the status of a location provider, use
+ * {@link LocationManager#isProviderEnabled(String)}.
*/
+ @Deprecated
public static final int LOCATION_MODE_SENSORS_ONLY = 1;
+
/**
* Reduced power usage, such as limiting the number of GPS updates per hour. Requests
* with {@link android.location.Criteria#POWER_HIGH} may be downgraded to
* {@link android.location.Criteria#POWER_MEDIUM}.
+ *
+ * @deprecated To check location status, use {@link LocationManager#isLocationEnabled()}. To
+ * get the status of a location provider, use
+ * {@link LocationManager#isProviderEnabled(String)}.
*/
+ @Deprecated
public static final int LOCATION_MODE_BATTERY_SAVING = 2;
+
/**
* Best-effort location computation allowed.
+ *
+ * @deprecated To check location status, use {@link LocationManager#isLocationEnabled()}. To
+ * get the status of a location provider, use
+ * {@link LocationManager#isProviderEnabled(String)}.
*/
+ @Deprecated
public static final int LOCATION_MODE_HIGH_ACCURACY = 3;
/**
@@ -5707,6 +5722,8 @@
@Deprecated
public static final String USB_MASS_STORAGE_ENABLED = Global.USB_MASS_STORAGE_ENABLED;
+ private static final Validator USB_MASS_STORAGE_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#USE_GOOGLE_MAIL} instead
*/
@@ -5718,6 +5735,8 @@
*/
public static final String ACCESSIBILITY_ENABLED = "accessibility_enabled";
+ private static final Validator ACCESSIBILITY_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Setting specifying if the accessibility shortcut is enabled.
* @hide
@@ -5725,6 +5744,8 @@
public static final String ACCESSIBILITY_SHORTCUT_ENABLED =
"accessibility_shortcut_enabled";
+ private static final Validator ACCESSIBILITY_SHORTCUT_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Setting specifying if the accessibility shortcut is enabled.
* @hide
@@ -5732,6 +5753,9 @@
public static final String ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN =
"accessibility_shortcut_on_lock_screen";
+ private static final Validator ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Setting specifying if the accessibility shortcut dialog has been shown to this user.
* @hide
@@ -5739,6 +5763,9 @@
public static final String ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN =
"accessibility_shortcut_dialog_shown";
+ private static final Validator ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Setting specifying the accessibility service to be toggled via the accessibility
* shortcut. Must be its flattened {@link ComponentName}.
@@ -5747,6 +5774,9 @@
public static final String ACCESSIBILITY_SHORTCUT_TARGET_SERVICE =
"accessibility_shortcut_target_service";
+ private static final Validator ACCESSIBILITY_SHORTCUT_TARGET_SERVICE_VALIDATOR =
+ COMPONENT_NAME_VALIDATOR;
+
/**
* Setting specifying the accessibility service or feature to be toggled via the
* accessibility button in the navigation bar. This is either a flattened
@@ -5757,17 +5787,32 @@
public static final String ACCESSIBILITY_BUTTON_TARGET_COMPONENT =
"accessibility_button_target_component";
+ private static final Validator ACCESSIBILITY_BUTTON_TARGET_COMPONENT_VALIDATOR =
+ new Validator() {
+ @Override
+ public boolean validate(String value) {
+ // technically either ComponentName or class name, but there's proper value
+ // validation at callsites, so allow any non-null string
+ return value != null;
+ }
+ };
+
/**
* If touch exploration is enabled.
*/
public static final String TOUCH_EXPLORATION_ENABLED = "touch_exploration_enabled";
+ private static final Validator TOUCH_EXPLORATION_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* List of the enabled accessibility providers.
*/
public static final String ENABLED_ACCESSIBILITY_SERVICES =
"enabled_accessibility_services";
+ private static final Validator ENABLED_ACCESSIBILITY_SERVICES_VALIDATOR =
+ new SettingsValidators.ComponentNameListValidator(":");
+
/**
* List of the accessibility services to which the user has granted
* permission to put the device into touch exploration mode.
@@ -5777,6 +5822,9 @@
public static final String TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES =
"touch_exploration_granted_accessibility_services";
+ private static final Validator TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES_VALIDATOR =
+ new SettingsValidators.ComponentNameListValidator(":");
+
/**
* Uri of the slice that's presented on the keyguard.
* Defaults to a slice with the date and next alarm.
@@ -5795,6 +5843,8 @@
@Deprecated
public static final String ACCESSIBILITY_SPEAK_PASSWORD = "speak_password";
+ private static final Validator ACCESSIBILITY_SPEAK_PASSWORD_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether to draw text with high contrast while in accessibility mode.
*
@@ -5803,6 +5853,9 @@
public static final String ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED =
"high_text_contrast_enabled";
+ private static final Validator ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Setting that specifies whether the display magnification is enabled via a system-wide
* triple tap gesture. Display magnifications allows the user to zoom in the display content
@@ -5815,6 +5868,9 @@
public static final String ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED =
"accessibility_display_magnification_enabled";
+ private static final Validator ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Setting that specifies whether the display magnification is enabled via a shortcut
* affordance within the system's navigation area. Display magnifications allows the user to
@@ -5826,6 +5882,9 @@
public static final String ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED =
"accessibility_display_magnification_navbar_enabled";
+ private static final Validator ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED_VALIDATOR
+ = BOOLEAN_VALIDATOR;
+
/**
* Setting that specifies what the display magnification scale is.
* Display magnifications allows the user to zoom in the display
@@ -5839,6 +5898,9 @@
public static final String ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE =
"accessibility_display_magnification_scale";
+ private static final Validator ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE_VALIDATOR =
+ new SettingsValidators.InclusiveFloatRangeValidator(1.0f, Float.MAX_VALUE);
+
/**
* Unused mangnification setting
*
@@ -5891,6 +5953,9 @@
public static final String ACCESSIBILITY_CAPTIONING_ENABLED =
"accessibility_captioning_enabled";
+ private static final Validator ACCESSIBILITY_CAPTIONING_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Setting that specifies the language for captions as a locale string,
* e.g. en_US.
@@ -5901,6 +5966,8 @@
public static final String ACCESSIBILITY_CAPTIONING_LOCALE =
"accessibility_captioning_locale";
+ private static final Validator ACCESSIBILITY_CAPTIONING_LOCALE_VALIDATOR = LOCALE_VALIDATOR;
+
/**
* Integer property that specifies the preset style for captions, one
* of:
@@ -5915,6 +5982,10 @@
public static final String ACCESSIBILITY_CAPTIONING_PRESET =
"accessibility_captioning_preset";
+ private static final Validator ACCESSIBILITY_CAPTIONING_PRESET_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[]{"-1", "0", "1", "2",
+ "3", "4"});
+
/**
* Integer property that specifes the background color for captions as a
* packed 32-bit color.
@@ -5925,6 +5996,9 @@
public static final String ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR =
"accessibility_captioning_background_color";
+ private static final Validator ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR_VALIDATOR =
+ ANY_INTEGER_VALIDATOR;
+
/**
* Integer property that specifes the foreground color for captions as a
* packed 32-bit color.
@@ -5935,6 +6009,9 @@
public static final String ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR =
"accessibility_captioning_foreground_color";
+ private static final Validator ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR_VALIDATOR =
+ ANY_INTEGER_VALIDATOR;
+
/**
* Integer property that specifes the edge type for captions, one of:
* <ul>
@@ -5949,6 +6026,9 @@
public static final String ACCESSIBILITY_CAPTIONING_EDGE_TYPE =
"accessibility_captioning_edge_type";
+ private static final Validator ACCESSIBILITY_CAPTIONING_EDGE_TYPE_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[]{"0", "1", "2"});
+
/**
* Integer property that specifes the edge color for captions as a
* packed 32-bit color.
@@ -5960,6 +6040,9 @@
public static final String ACCESSIBILITY_CAPTIONING_EDGE_COLOR =
"accessibility_captioning_edge_color";
+ private static final Validator ACCESSIBILITY_CAPTIONING_EDGE_COLOR_VALIDATOR =
+ ANY_INTEGER_VALIDATOR;
+
/**
* Integer property that specifes the window color for captions as a
* packed 32-bit color.
@@ -5970,6 +6053,9 @@
public static final String ACCESSIBILITY_CAPTIONING_WINDOW_COLOR =
"accessibility_captioning_window_color";
+ private static final Validator ACCESSIBILITY_CAPTIONING_WINDOW_COLOR_VALIDATOR =
+ ANY_INTEGER_VALIDATOR;
+
/**
* String property that specifies the typeface for captions, one of:
* <ul>
@@ -5985,6 +6071,10 @@
public static final String ACCESSIBILITY_CAPTIONING_TYPEFACE =
"accessibility_captioning_typeface";
+ private static final Validator ACCESSIBILITY_CAPTIONING_TYPEFACE_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[]{"DEFAULT",
+ "MONOSPACE", "SANS_SERIF", "SERIF"});
+
/**
* Floating point property that specifies font scaling for captions.
*
@@ -5993,12 +6083,18 @@
public static final String ACCESSIBILITY_CAPTIONING_FONT_SCALE =
"accessibility_captioning_font_scale";
+ private static final Validator ACCESSIBILITY_CAPTIONING_FONT_SCALE_VALIDATOR =
+ new SettingsValidators.InclusiveFloatRangeValidator(0.5f, 2.0f);
+
/**
* Setting that specifies whether display color inversion is enabled.
*/
public static final String ACCESSIBILITY_DISPLAY_INVERSION_ENABLED =
"accessibility_display_inversion_enabled";
+ private static final Validator ACCESSIBILITY_DISPLAY_INVERSION_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Setting that specifies whether display color space adjustment is
* enabled.
@@ -6008,15 +6104,24 @@
public static final String ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED =
"accessibility_display_daltonizer_enabled";
+ private static final Validator ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Integer property that specifies the type of color space adjustment to
- * perform. Valid values are defined in AccessibilityManager.
+ * perform. Valid values are defined in AccessibilityManager:
+ * - AccessibilityManager.DALTONIZER_DISABLED = -1
+ * - AccessibilityManager.DALTONIZER_SIMULATE_MONOCHROMACY = 0
+ * - AccessibilityManager.DALTONIZER_CORRECT_DEUTERANOMALY = 12
*
* @hide
*/
public static final String ACCESSIBILITY_DISPLAY_DALTONIZER =
"accessibility_display_daltonizer";
+ private static final Validator ACCESSIBILITY_DISPLAY_DALTONIZER_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[] {"-1", "0", "12"});
+
/**
* Setting that specifies whether automatic click when the mouse pointer stops moving is
* enabled.
@@ -6026,6 +6131,9 @@
public static final String ACCESSIBILITY_AUTOCLICK_ENABLED =
"accessibility_autoclick_enabled";
+ private static final Validator ACCESSIBILITY_AUTOCLICK_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Integer setting specifying amount of time in ms the mouse pointer has to stay still
* before performing click when {@link #ACCESSIBILITY_AUTOCLICK_ENABLED} is set.
@@ -6036,6 +6144,9 @@
public static final String ACCESSIBILITY_AUTOCLICK_DELAY =
"accessibility_autoclick_delay";
+ private static final Validator ACCESSIBILITY_AUTOCLICK_DELAY_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* Whether or not larger size icons are used for the pointer of mouse/trackpad for
* accessibility.
@@ -6045,12 +6156,18 @@
public static final String ACCESSIBILITY_LARGE_POINTER_ICON =
"accessibility_large_pointer_icon";
+ private static final Validator ACCESSIBILITY_LARGE_POINTER_ICON_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* The timeout for considering a press to be a long press in milliseconds.
* @hide
*/
public static final String LONG_PRESS_TIMEOUT = "long_press_timeout";
+ private static final Validator LONG_PRESS_TIMEOUT_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* The duration in milliseconds between the first tap's up event and the second tap's
* down event for an interaction to be considered part of the same multi-press.
@@ -6104,16 +6221,22 @@
*/
public static final String TTS_DEFAULT_RATE = "tts_default_rate";
+ private static final Validator TTS_DEFAULT_RATE_VALIDATOR = NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* Default text-to-speech engine pitch. 100 = 1x
*/
public static final String TTS_DEFAULT_PITCH = "tts_default_pitch";
+ private static final Validator TTS_DEFAULT_PITCH_VALIDATOR = NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* Default text-to-speech engine.
*/
public static final String TTS_DEFAULT_SYNTH = "tts_default_synth";
+ private static final Validator TTS_DEFAULT_SYNTH_VALIDATOR = PACKAGE_NAME_VALIDATOR;
+
/**
* Default text-to-speech language.
*
@@ -6161,11 +6284,33 @@
*/
public static final String TTS_DEFAULT_LOCALE = "tts_default_locale";
+ private static final Validator TTS_DEFAULT_LOCALE_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ if (value == null || value.length() == 0) {
+ return false;
+ }
+ String[] ttsLocales = value.split(",");
+ boolean valid = true;
+ for (String ttsLocale : ttsLocales) {
+ String[] parts = ttsLocale.split(":");
+ valid |= ((parts.length == 2)
+ && (parts[0].length() > 0)
+ && ANY_STRING_VALIDATOR.validate(parts[0])
+ && LOCALE_VALIDATOR.validate(parts[1]));
+ }
+ return valid;
+ }
+ };
+
/**
* Space delimited list of plugin packages that are enabled.
*/
public static final String TTS_ENABLED_PLUGINS = "tts_enabled_plugins";
+ private static final Validator TTS_ENABLED_PLUGINS_VALIDATOR =
+ new SettingsValidators.PackageNameListValidator(" ");
+
/**
* @deprecated Use {@link android.provider.Settings.Global#WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON}
* instead.
@@ -6174,6 +6319,9 @@
public static final String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON =
Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON;
+ private static final Validator WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY}
* instead.
@@ -6182,6 +6330,9 @@
public static final String WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY =
Global.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY;
+ private static final Validator WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#WIFI_NUM_OPEN_NETWORKS_KEPT}
* instead.
@@ -6190,6 +6341,9 @@
public static final String WIFI_NUM_OPEN_NETWORKS_KEPT =
Global.WIFI_NUM_OPEN_NETWORKS_KEPT;
+ private static final Validator WIFI_NUM_OPEN_NETWORKS_KEPT_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#WIFI_ON}
* instead.
@@ -6348,6 +6502,9 @@
public static final String PREFERRED_TTY_MODE =
"preferred_tty_mode";
+ private static final Validator PREFERRED_TTY_MODE_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[]{"0", "1", "2", "3"});
+
/**
* Whether the enhanced voice privacy mode is enabled.
* 0 = normal voice privacy
@@ -6356,6 +6513,8 @@
*/
public static final String ENHANCED_VOICE_PRIVACY_ENABLED = "enhanced_voice_privacy_enabled";
+ private static final Validator ENHANCED_VOICE_PRIVACY_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether the TTY mode mode is enabled.
* 0 = disabled
@@ -6364,6 +6523,8 @@
*/
public static final String TTY_MODE_ENABLED = "tty_mode_enabled";
+ private static final Validator TTY_MODE_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Controls whether settings backup is enabled.
* Type: int ( 0 = disabled, 1 = enabled )
@@ -6534,24 +6695,32 @@
*/
public static final String MOUNT_PLAY_NOTIFICATION_SND = "mount_play_not_snd";
+ private static final Validator MOUNT_PLAY_NOTIFICATION_SND_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether or not UMS auto-starts on UMS host detection. (0 = false, 1 = true)
* @hide
*/
public static final String MOUNT_UMS_AUTOSTART = "mount_ums_autostart";
+ private static final Validator MOUNT_UMS_AUTOSTART_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether or not a notification is displayed on UMS host detection. (0 = false, 1 = true)
* @hide
*/
public static final String MOUNT_UMS_PROMPT = "mount_ums_prompt";
+ private static final Validator MOUNT_UMS_PROMPT_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether or not a notification is displayed while UMS is enabled. (0 = false, 1 = true)
* @hide
*/
public static final String MOUNT_UMS_NOTIFY_ENABLED = "mount_ums_notify_enabled";
+ private static final Validator MOUNT_UMS_NOTIFY_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* If nonzero, ANRs in invisible background processes bring up a dialog.
* Otherwise, the process will be silently killed.
@@ -6562,6 +6731,17 @@
public static final String ANR_SHOW_BACKGROUND = "anr_show_background";
/**
+ * If nonzero, crashes in foreground processes will bring up a dialog.
+ * Otherwise, the process will be silently killed.
+ * @hide
+ */
+ public static final String SHOW_FIRST_CRASH_DIALOG_DEV_OPTION =
+ "show_first_crash_dialog_dev_option";
+
+ private static final Validator SHOW_FIRST_CRASH_DIALOG_DEV_OPTION_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
+ /**
* The {@link ComponentName} string of the service to be used as the voice recognition
* service.
*
@@ -6586,6 +6766,8 @@
*/
public static final String SELECTED_SPELL_CHECKER = "selected_spell_checker";
+ private static final Validator SELECTED_SPELL_CHECKER_VALIDATOR = COMPONENT_NAME_VALIDATOR;
+
/**
* The {@link ComponentName} string of the selected subtype of the selected spell checker
* service which is one of the services managed by the text service manager.
@@ -6595,13 +6777,18 @@
public static final String SELECTED_SPELL_CHECKER_SUBTYPE =
"selected_spell_checker_subtype";
+ private static final Validator SELECTED_SPELL_CHECKER_SUBTYPE_VALIDATOR =
+ COMPONENT_NAME_VALIDATOR;
+
/**
- * The {@link ComponentName} string whether spell checker is enabled or not.
+ * Whether spell checker is enabled or not.
*
* @hide
*/
public static final String SPELL_CHECKER_ENABLED = "spell_checker_enabled";
+ private static final Validator SPELL_CHECKER_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* What happens when the user presses the Power button while in-call
* and the screen is on.<br/>
@@ -6613,6 +6800,9 @@
*/
public static final String INCALL_POWER_BUTTON_BEHAVIOR = "incall_power_button_behavior";
+ private static final Validator INCALL_POWER_BUTTON_BEHAVIOR_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[]{"1", "2"});
+
/**
* INCALL_POWER_BUTTON_BEHAVIOR value for "turn off screen".
* @hide
@@ -6668,12 +6858,16 @@
*/
public static final String WAKE_GESTURE_ENABLED = "wake_gesture_enabled";
+ private static final Validator WAKE_GESTURE_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether the device should doze if configured.
* @hide
*/
public static final String DOZE_ENABLED = "doze_enabled";
+ private static final Validator DOZE_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether doze should be always on.
* @hide
@@ -6686,6 +6880,8 @@
*/
public static final String DOZE_PULSE_ON_PICK_UP = "doze_pulse_on_pick_up";
+ private static final Validator DOZE_PULSE_ON_PICK_UP_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether the device should pulse on long press gesture.
* @hide
@@ -6698,6 +6894,8 @@
*/
public static final String DOZE_PULSE_ON_DOUBLE_TAP = "doze_pulse_on_double_tap";
+ private static final Validator DOZE_PULSE_ON_DOUBLE_TAP_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* The current night mode that has been selected by the user. Owned
* and controlled by UiModeManagerService. Constants are as per
@@ -6712,6 +6910,8 @@
*/
public static final String SCREENSAVER_ENABLED = "screensaver_enabled";
+ private static final Validator SCREENSAVER_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* The user's chosen screensaver components.
*
@@ -6721,6 +6921,9 @@
*/
public static final String SCREENSAVER_COMPONENTS = "screensaver_components";
+ private static final Validator SCREENSAVER_COMPONENTS_VALIDATOR =
+ new SettingsValidators.ComponentNameListValidator(",");
+
/**
* If screensavers are enabled, whether the screensaver should be automatically launched
* when the device is inserted into a (desk) dock.
@@ -6728,6 +6931,8 @@
*/
public static final String SCREENSAVER_ACTIVATE_ON_DOCK = "screensaver_activate_on_dock";
+ private static final Validator SCREENSAVER_ACTIVATE_ON_DOCK_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* If screensavers are enabled, whether the screensaver should be automatically launched
* when the screen times out when not on battery.
@@ -6735,6 +6940,8 @@
*/
public static final String SCREENSAVER_ACTIVATE_ON_SLEEP = "screensaver_activate_on_sleep";
+ private static final Validator SCREENSAVER_ACTIVATE_ON_SLEEP_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* If screensavers are enabled, the default screensaver component.
* @hide
@@ -6747,6 +6954,9 @@
*/
public static final String NFC_PAYMENT_DEFAULT_COMPONENT = "nfc_payment_default_component";
+ private static final Validator NFC_PAYMENT_DEFAULT_COMPONENT_VALIDATOR =
+ COMPONENT_NAME_VALIDATOR;
+
/**
* Whether NFC payment is handled by the foreground application or a default.
* @hide
@@ -6802,6 +7012,37 @@
public static final String ASSIST_DISCLOSURE_ENABLED = "assist_disclosure_enabled";
/**
+ * Control if rotation suggestions are sent to System UI when in rotation locked mode.
+ * Done to enable screen rotation while the the screen rotation is locked. Enabling will
+ * poll the accelerometer in rotation locked mode.
+ *
+ * If 0, then rotation suggestions are not sent to System UI. If 1, suggestions are sent.
+ *
+ * @hide
+ */
+
+ public static final String SHOW_ROTATION_SUGGESTIONS = "show_rotation_suggestions";
+
+ /**
+ * The disabled state of SHOW_ROTATION_SUGGESTIONS.
+ * @hide
+ */
+ public static final int SHOW_ROTATION_SUGGESTIONS_DISABLED = 0x0;
+
+ /**
+ * The enabled state of SHOW_ROTATION_SUGGESTIONS.
+ * @hide
+ */
+ public static final int SHOW_ROTATION_SUGGESTIONS_ENABLED = 0x1;
+
+ /**
+ * The default state of SHOW_ROTATION_SUGGESTIONS.
+ * @hide
+ */
+ public static final int SHOW_ROTATION_SUGGESTIONS_DEFAULT =
+ SHOW_ROTATION_SUGGESTIONS_ENABLED;
+
+ /**
* Read only list of the service components that the current user has explicitly allowed to
* see and assist with all of the user's notifications.
*
@@ -6813,6 +7054,9 @@
public static final String ENABLED_NOTIFICATION_ASSISTANT =
"enabled_notification_assistant";
+ private static final Validator ENABLED_NOTIFICATION_ASSISTANT_VALIDATOR =
+ new SettingsValidators.ComponentNameListValidator(":");
+
/**
* Read only list of the service components that the current user has explicitly allowed to
* see all of the user's notifications, separated by ':'.
@@ -6824,6 +7068,9 @@
@Deprecated
public static final String ENABLED_NOTIFICATION_LISTENERS = "enabled_notification_listeners";
+ private static final Validator ENABLED_NOTIFICATION_LISTENERS_VALIDATOR =
+ new SettingsValidators.ComponentNameListValidator(":");
+
/**
* Read only list of the packages that the current user has explicitly allowed to
* manage do not disturb, separated by ':'.
@@ -6836,6 +7083,9 @@
public static final String ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES =
"enabled_notification_policy_access_packages";
+ private static final Validator ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES_VALIDATOR =
+ new SettingsValidators.PackageNameListValidator(":");
+
/**
* Defines whether managed profile ringtones should be synced from it's parent profile
* <p>
@@ -6849,6 +7099,8 @@
@RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
public static final String SYNC_PARENT_SOUNDS = "sync_parent_sounds";
+ private static final Validator SYNC_PARENT_SOUNDS_VALIDATOR = BOOLEAN_VALIDATOR;
+
/** @hide */
public static final String IMMERSIVE_MODE_CONFIRMATIONS = "immersive_mode_confirmations";
@@ -6935,12 +7187,17 @@
*/
public static final String SLEEP_TIMEOUT = "sleep_timeout";
+ private static final Validator SLEEP_TIMEOUT_VALIDATOR =
+ new SettingsValidators.InclusiveIntegerRangeValidator(-1, Integer.MAX_VALUE);
+
/**
* Controls whether double tap to wake is enabled.
* @hide
*/
public static final String DOUBLE_TAP_TO_WAKE = "double_tap_to_wake";
+ private static final Validator DOUBLE_TAP_TO_WAKE_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* The current assistant component. It could be a voice interaction service,
* or an activity that handles ACTION_ASSIST, or empty which means using the default
@@ -6957,6 +7214,8 @@
*/
public static final String CAMERA_GESTURE_DISABLED = "camera_gesture_disabled";
+ private static final Validator CAMERA_GESTURE_DISABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether the camera launch gesture to double tap the power button when the screen is off
* should be disabled.
@@ -6966,6 +7225,9 @@
public static final String CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED =
"camera_double_tap_power_gesture_disabled";
+ private static final Validator CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Whether the camera double twist gesture to flip between front and back mode should be
* enabled.
@@ -6975,6 +7237,9 @@
public static final String CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED =
"camera_double_twist_to_flip_enabled";
+ private static final Validator CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Whether or not the smart camera lift trigger that launches the camera when the user moves
* the phone into a position for taking photos should be enabled.
@@ -6997,6 +7262,9 @@
*/
public static final String ASSIST_GESTURE_ENABLED = "assist_gesture_enabled";
+ private static final Validator ASSIST_GESTURE_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Sensitivity control for the assist gesture.
*
@@ -7004,6 +7272,9 @@
*/
public static final String ASSIST_GESTURE_SENSITIVITY = "assist_gesture_sensitivity";
+ private static final Validator ASSIST_GESTURE_SENSITIVITY_VALIDATOR =
+ new SettingsValidators.InclusiveFloatRangeValidator(0.0f, 1.0f);
+
/**
* Whether the assist gesture should silence alerts.
*
@@ -7012,6 +7283,9 @@
public static final String ASSIST_GESTURE_SILENCE_ALERTS_ENABLED =
"assist_gesture_silence_alerts_enabled";
+ private static final Validator ASSIST_GESTURE_SILENCE_ALERTS_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Whether the assist gesture should wake the phone.
*
@@ -7020,6 +7294,9 @@
public static final String ASSIST_GESTURE_WAKE_ENABLED =
"assist_gesture_wake_enabled";
+ private static final Validator ASSIST_GESTURE_WAKE_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Whether Assist Gesture Deferred Setup has been completed
*
@@ -7027,6 +7304,8 @@
*/
public static final String ASSIST_GESTURE_SETUP_COMPLETE = "assist_gesture_setup_complete";
+ private static final Validator ASSIST_GESTURE_SETUP_COMPLETE_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Control whether Night display is currently activated.
* @hide
@@ -7039,6 +7318,8 @@
*/
public static final String NIGHT_DISPLAY_AUTO_MODE = "night_display_auto_mode";
+ private static final Validator NIGHT_DISPLAY_AUTO_MODE_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Control the color temperature of Night Display, represented in Kelvin.
* @hide
@@ -7046,6 +7327,9 @@
public static final String NIGHT_DISPLAY_COLOR_TEMPERATURE =
"night_display_color_temperature";
+ private static final Validator NIGHT_DISPLAY_COLOR_TEMPERATURE_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* Custom time when Night display is scheduled to activate.
* Represented as milliseconds from midnight (e.g. 79200000 == 10pm).
@@ -7054,6 +7338,9 @@
public static final String NIGHT_DISPLAY_CUSTOM_START_TIME =
"night_display_custom_start_time";
+ private static final Validator NIGHT_DISPLAY_CUSTOM_START_TIME_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* Custom time when Night display is scheduled to deactivate.
* Represented as milliseconds from midnight (e.g. 21600000 == 6am).
@@ -7061,6 +7348,9 @@
*/
public static final String NIGHT_DISPLAY_CUSTOM_END_TIME = "night_display_custom_end_time";
+ private static final Validator NIGHT_DISPLAY_CUSTOM_END_TIME_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* A String representing the LocalDateTime when Night display was last activated. Use to
* decide whether to apply the current activated state after a reboot or user change. In
@@ -7078,6 +7368,9 @@
*/
public static final String ENABLED_VR_LISTENERS = "enabled_vr_listeners";
+ private static final Validator ENABLED_VR_LISTENERS_VALIDATOR =
+ new SettingsValidators.ComponentNameListValidator(":");
+
/**
* Behavior of the display while in VR mode.
*
@@ -7087,6 +7380,9 @@
*/
public static final String VR_DISPLAY_MODE = "vr_display_mode";
+ private static final Validator VR_DISPLAY_MODE_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[]{"0", "1"});
+
/**
* Lower the display persistence while the system is in VR mode.
*
@@ -7143,6 +7439,9 @@
public static final String AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN =
"automatic_storage_manager_days_to_retain";
+ private static final Validator AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* Default number of days of information for the automatic storage manager to retain.
*
@@ -7184,18 +7483,29 @@
public static final String SYSTEM_NAVIGATION_KEYS_ENABLED =
"system_navigation_keys_enabled";
+ private static final Validator SYSTEM_NAVIGATION_KEYS_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Holds comma separated list of ordering of QS tiles.
* @hide
*/
public static final String QS_TILES = "sysui_qs_tiles";
- /**
- * Whether preloaded APKs have been installed for the user.
- * @hide
- */
- public static final String DEMO_USER_SETUP_COMPLETE
- = "demo_user_setup_complete";
+ private static final Validator QS_TILES_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ if (value == null) {
+ return false;
+ }
+ String[] tiles = value.split(",");
+ boolean valid = true;
+ for (String tile : tiles) {
+ // tile can be any non-empty string as specified by OEM
+ valid |= ((tile.length() > 0) && ANY_STRING_VALIDATOR.validate(tile));
+ }
+ return valid;
+ }
+ };
/**
* Specifies whether the web action API is enabled.
@@ -7232,18 +7542,38 @@
*/
public static final String NOTIFICATION_BADGING = "notification_badging";
+ private static final Validator NOTIFICATION_BADGING_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Comma separated list of QS tiles that have been auto-added already.
* @hide
*/
public static final String QS_AUTO_ADDED_TILES = "qs_auto_tiles";
+ private static final Validator QS_AUTO_ADDED_TILES_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ if (value == null) {
+ return false;
+ }
+ String[] tiles = value.split(",");
+ boolean valid = true;
+ for (String tile : tiles) {
+ // tile can be any non-empty string as specified by OEM
+ valid |= ((tile.length() > 0) && ANY_STRING_VALIDATOR.validate(tile));
+ }
+ return valid;
+ }
+ };
+
/**
* Whether the Lockdown button should be shown in the power menu.
* @hide
*/
public static final String LOCKDOWN_IN_POWER_MENU = "lockdown_in_power_menu";
+ private static final Validator LOCKDOWN_IN_POWER_MENU_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Backup manager behavioral parameters.
* This is encoded as a key=value list, separated by commas. Ex:
@@ -7272,6 +7602,13 @@
public static final String BACKUP_MANAGER_CONSTANTS = "backup_manager_constants";
/**
+ * Flag to set if the system should predictively attempt to re-enable Bluetooth while
+ * the user is driving.
+ * @hide
+ */
+ public static final String BLUETOOTH_ON_WHILE_DRIVING = "bluetooth_on_while_driving";
+
+ /**
* This are the settings to be backed up.
*
* NOTE: Settings are backed up and restored in the order they appear
@@ -7283,8 +7620,6 @@
public static final String[] SETTINGS_TO_BACKUP = {
BUGREPORT_IN_POWER_MENU, // moved to global
ALLOW_MOCK_LOCATION,
- PARENTAL_CONTROL_ENABLED,
- PARENTAL_CONTROL_REDIRECT_URL,
USB_MASS_STORAGE_ENABLED, // moved to global
ACCESSIBILITY_DISPLAY_INVERSION_ENABLED,
ACCESSIBILITY_DISPLAY_DALTONIZER,
@@ -7316,12 +7651,9 @@
ACCESSIBILITY_CAPTIONING_TYPEFACE,
ACCESSIBILITY_CAPTIONING_FONT_SCALE,
ACCESSIBILITY_CAPTIONING_WINDOW_COLOR,
- TTS_USE_DEFAULTS,
TTS_DEFAULT_RATE,
TTS_DEFAULT_PITCH,
TTS_DEFAULT_SYNTH,
- TTS_DEFAULT_LANG,
- TTS_DEFAULT_COUNTRY,
TTS_ENABLED_PLUGINS,
TTS_DEFAULT_LOCALE,
SHOW_IME_WITH_HARD_KEYBOARD,
@@ -7374,9 +7706,161 @@
SCREENSAVER_ACTIVATE_ON_DOCK,
SCREENSAVER_ACTIVATE_ON_SLEEP,
LOCKDOWN_IN_POWER_MENU,
+ SHOW_FIRST_CRASH_DIALOG_DEV_OPTION,
};
- /** @hide */
+ /**
+ * All settings in {@link SETTINGS_TO_BACKUP} array *must* have a non-null validator,
+ * otherwise they won't be restored.
+ *
+ * @hide
+ */
+ public static final Map<String, Validator> VALIDATORS = new ArrayMap<>();
+ static {
+ VALIDATORS.put(BUGREPORT_IN_POWER_MENU, BUGREPORT_IN_POWER_MENU_VALIDATOR);
+ VALIDATORS.put(ALLOW_MOCK_LOCATION, ALLOW_MOCK_LOCATION_VALIDATOR);
+ VALIDATORS.put(USB_MASS_STORAGE_ENABLED, USB_MASS_STORAGE_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_DISPLAY_INVERSION_ENABLED,
+ ACCESSIBILITY_DISPLAY_INVERSION_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_DISPLAY_DALTONIZER,
+ ACCESSIBILITY_DISPLAY_DALTONIZER_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
+ ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED,
+ ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED,
+ ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED_VALIDATOR);
+ VALIDATORS.put(AUTOFILL_SERVICE, AUTOFILL_SERVICE_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
+ ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE_VALIDATOR);
+ VALIDATORS.put(ENABLED_ACCESSIBILITY_SERVICES,
+ ENABLED_ACCESSIBILITY_SERVICES_VALIDATOR);
+ VALIDATORS.put(ENABLED_VR_LISTENERS, ENABLED_VR_LISTENERS_VALIDATOR);
+ VALIDATORS.put(ENABLED_INPUT_METHODS, ENABLED_INPUT_METHODS_VALIDATOR);
+ VALIDATORS.put(TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES,
+ TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES_VALIDATOR);
+ VALIDATORS.put(TOUCH_EXPLORATION_ENABLED, TOUCH_EXPLORATION_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_ENABLED, ACCESSIBILITY_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
+ ACCESSIBILITY_SHORTCUT_TARGET_SERVICE_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_BUTTON_TARGET_COMPONENT,
+ ACCESSIBILITY_BUTTON_TARGET_COMPONENT_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN,
+ ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_SHORTCUT_ENABLED,
+ ACCESSIBILITY_SHORTCUT_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN,
+ ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_SPEAK_PASSWORD, ACCESSIBILITY_SPEAK_PASSWORD_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED,
+ ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_PRESET,
+ ACCESSIBILITY_CAPTIONING_PRESET_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_ENABLED,
+ ACCESSIBILITY_CAPTIONING_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_LOCALE,
+ ACCESSIBILITY_CAPTIONING_LOCALE_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR,
+ ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR,
+ ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_EDGE_TYPE,
+ ACCESSIBILITY_CAPTIONING_EDGE_TYPE_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_EDGE_COLOR,
+ ACCESSIBILITY_CAPTIONING_EDGE_COLOR_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_TYPEFACE,
+ ACCESSIBILITY_CAPTIONING_TYPEFACE_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_FONT_SCALE,
+ ACCESSIBILITY_CAPTIONING_FONT_SCALE_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_WINDOW_COLOR,
+ ACCESSIBILITY_CAPTIONING_WINDOW_COLOR_VALIDATOR);
+ VALIDATORS.put(TTS_DEFAULT_RATE, TTS_DEFAULT_RATE_VALIDATOR);
+ VALIDATORS.put(TTS_DEFAULT_PITCH, TTS_DEFAULT_PITCH_VALIDATOR);
+ VALIDATORS.put(TTS_DEFAULT_SYNTH, TTS_DEFAULT_SYNTH_VALIDATOR);
+ VALIDATORS.put(TTS_ENABLED_PLUGINS, TTS_ENABLED_PLUGINS_VALIDATOR);
+ VALIDATORS.put(TTS_DEFAULT_LOCALE, TTS_DEFAULT_LOCALE_VALIDATOR);
+ VALIDATORS.put(SHOW_IME_WITH_HARD_KEYBOARD, SHOW_IME_WITH_HARD_KEYBOARD_VALIDATOR);
+ VALIDATORS.put(WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON,
+ WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON_VALIDATOR);
+ VALIDATORS.put(WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY,
+ WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY_VALIDATOR);
+ VALIDATORS.put(WIFI_NUM_OPEN_NETWORKS_KEPT, WIFI_NUM_OPEN_NETWORKS_KEPT_VALIDATOR);
+ VALIDATORS.put(SELECTED_SPELL_CHECKER, SELECTED_SPELL_CHECKER_VALIDATOR);
+ VALIDATORS.put(SELECTED_SPELL_CHECKER_SUBTYPE,
+ SELECTED_SPELL_CHECKER_SUBTYPE_VALIDATOR);
+ VALIDATORS.put(SPELL_CHECKER_ENABLED, SPELL_CHECKER_ENABLED_VALIDATOR);
+ VALIDATORS.put(MOUNT_PLAY_NOTIFICATION_SND, MOUNT_PLAY_NOTIFICATION_SND_VALIDATOR);
+ VALIDATORS.put(MOUNT_UMS_AUTOSTART, MOUNT_UMS_AUTOSTART_VALIDATOR);
+ VALIDATORS.put(MOUNT_UMS_PROMPT, MOUNT_UMS_PROMPT_VALIDATOR);
+ VALIDATORS.put(MOUNT_UMS_NOTIFY_ENABLED, MOUNT_UMS_NOTIFY_ENABLED_VALIDATOR);
+ VALIDATORS.put(SLEEP_TIMEOUT, SLEEP_TIMEOUT_VALIDATOR);
+ VALIDATORS.put(DOUBLE_TAP_TO_WAKE, DOUBLE_TAP_TO_WAKE_VALIDATOR);
+ VALIDATORS.put(WAKE_GESTURE_ENABLED, WAKE_GESTURE_ENABLED_VALIDATOR);
+ VALIDATORS.put(LONG_PRESS_TIMEOUT, LONG_PRESS_TIMEOUT_VALIDATOR);
+ VALIDATORS.put(CAMERA_GESTURE_DISABLED, CAMERA_GESTURE_DISABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_AUTOCLICK_ENABLED,
+ ACCESSIBILITY_AUTOCLICK_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_AUTOCLICK_DELAY, ACCESSIBILITY_AUTOCLICK_DELAY_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_LARGE_POINTER_ICON,
+ ACCESSIBILITY_LARGE_POINTER_ICON_VALIDATOR);
+ VALIDATORS.put(PREFERRED_TTY_MODE, PREFERRED_TTY_MODE_VALIDATOR);
+ VALIDATORS.put(ENHANCED_VOICE_PRIVACY_ENABLED,
+ ENHANCED_VOICE_PRIVACY_ENABLED_VALIDATOR);
+ VALIDATORS.put(TTY_MODE_ENABLED, TTY_MODE_ENABLED_VALIDATOR);
+ VALIDATORS.put(INCALL_POWER_BUTTON_BEHAVIOR, INCALL_POWER_BUTTON_BEHAVIOR_VALIDATOR);
+ VALIDATORS.put(NIGHT_DISPLAY_CUSTOM_START_TIME,
+ NIGHT_DISPLAY_CUSTOM_START_TIME_VALIDATOR);
+ VALIDATORS.put(NIGHT_DISPLAY_CUSTOM_END_TIME, NIGHT_DISPLAY_CUSTOM_END_TIME_VALIDATOR);
+ VALIDATORS.put(NIGHT_DISPLAY_COLOR_TEMPERATURE,
+ NIGHT_DISPLAY_COLOR_TEMPERATURE_VALIDATOR);
+ VALIDATORS.put(NIGHT_DISPLAY_AUTO_MODE, NIGHT_DISPLAY_AUTO_MODE_VALIDATOR);
+ VALIDATORS.put(SYNC_PARENT_SOUNDS, SYNC_PARENT_SOUNDS_VALIDATOR);
+ VALIDATORS.put(CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED,
+ CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED_VALIDATOR);
+ VALIDATORS.put(CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED,
+ CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED_VALIDATOR);
+ VALIDATORS.put(SYSTEM_NAVIGATION_KEYS_ENABLED,
+ SYSTEM_NAVIGATION_KEYS_ENABLED_VALIDATOR);
+ VALIDATORS.put(QS_TILES, QS_TILES_VALIDATOR);
+ VALIDATORS.put(DOZE_ENABLED, DOZE_ENABLED_VALIDATOR);
+ VALIDATORS.put(DOZE_PULSE_ON_PICK_UP, DOZE_PULSE_ON_PICK_UP_VALIDATOR);
+ VALIDATORS.put(DOZE_PULSE_ON_DOUBLE_TAP, DOZE_PULSE_ON_DOUBLE_TAP_VALIDATOR);
+ VALIDATORS.put(NFC_PAYMENT_DEFAULT_COMPONENT, NFC_PAYMENT_DEFAULT_COMPONENT_VALIDATOR);
+ VALIDATORS.put(AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN,
+ AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN_VALIDATOR);
+ VALIDATORS.put(ASSIST_GESTURE_ENABLED, ASSIST_GESTURE_ENABLED_VALIDATOR);
+ VALIDATORS.put(ASSIST_GESTURE_SENSITIVITY, ASSIST_GESTURE_SENSITIVITY_VALIDATOR);
+ VALIDATORS.put(ASSIST_GESTURE_SETUP_COMPLETE, ASSIST_GESTURE_SETUP_COMPLETE_VALIDATOR);
+ VALIDATORS.put(ASSIST_GESTURE_SILENCE_ALERTS_ENABLED,
+ ASSIST_GESTURE_SILENCE_ALERTS_ENABLED_VALIDATOR);
+ VALIDATORS.put(ASSIST_GESTURE_WAKE_ENABLED, ASSIST_GESTURE_WAKE_ENABLED_VALIDATOR);
+ VALIDATORS.put(VR_DISPLAY_MODE, VR_DISPLAY_MODE_VALIDATOR);
+ VALIDATORS.put(NOTIFICATION_BADGING, NOTIFICATION_BADGING_VALIDATOR);
+ VALIDATORS.put(QS_AUTO_ADDED_TILES, QS_AUTO_ADDED_TILES_VALIDATOR);
+ VALIDATORS.put(SCREENSAVER_ENABLED, SCREENSAVER_ENABLED_VALIDATOR);
+ VALIDATORS.put(SCREENSAVER_COMPONENTS, SCREENSAVER_COMPONENTS_VALIDATOR);
+ VALIDATORS.put(SCREENSAVER_ACTIVATE_ON_DOCK, SCREENSAVER_ACTIVATE_ON_DOCK_VALIDATOR);
+ VALIDATORS.put(SCREENSAVER_ACTIVATE_ON_SLEEP, SCREENSAVER_ACTIVATE_ON_SLEEP_VALIDATOR);
+ VALIDATORS.put(LOCKDOWN_IN_POWER_MENU, LOCKDOWN_IN_POWER_MENU_VALIDATOR);
+ VALIDATORS.put(SHOW_FIRST_CRASH_DIALOG_DEV_OPTION,
+ SHOW_FIRST_CRASH_DIALOG_DEV_OPTION_VALIDATOR);
+ VALIDATORS.put(ENABLED_NOTIFICATION_LISTENERS,
+ ENABLED_NOTIFICATION_LISTENERS_VALIDATOR); //legacy restore setting
+ VALIDATORS.put(ENABLED_NOTIFICATION_ASSISTANT,
+ ENABLED_NOTIFICATION_ASSISTANT_VALIDATOR); //legacy restore setting
+ VALIDATORS.put(ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES,
+ ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES_VALIDATOR); //legacy restore setting
+ }
+
+ /**
+ * Keys we no longer back up under the current schema, but want to continue to
+ * process when restoring historical backup datasets.
+ *
+ * All settings in {@link LEGACY_RESTORE_SETTINGS} array *must* have a non-null validator,
+ * otherwise they won't be restored.
+ *
+ * @hide
+ */
public static final String[] LEGACY_RESTORE_SETTINGS = {
ENABLED_NOTIFICATION_LISTENERS,
ENABLED_NOTIFICATION_ASSISTANT,
@@ -7398,7 +7882,6 @@
CLONE_TO_MANAGED_PROFILE.add(ENABLED_ACCESSIBILITY_SERVICES);
CLONE_TO_MANAGED_PROFILE.add(ENABLED_INPUT_METHODS);
CLONE_TO_MANAGED_PROFILE.add(LOCATION_MODE);
- CLONE_TO_MANAGED_PROFILE.add(LOCATION_PREVIOUS_MODE);
CLONE_TO_MANAGED_PROFILE.add(LOCATION_PROVIDERS_ALLOWED);
CLONE_TO_MANAGED_PROFILE.add(SELECTED_INPUT_METHOD_SUBTYPE);
}
@@ -7449,8 +7932,7 @@
* @param provider the location provider to query
* @return true if the provider is enabled
*
- * @deprecated use {@link #LOCATION_MODE} or
- * {@link LocationManager#isProviderEnabled(String)}
+ * @deprecated use {@link LocationManager#isProviderEnabled(String)}
*/
@Deprecated
public static final boolean isLocationProviderEnabled(ContentResolver cr, String provider) {
@@ -7463,12 +7945,13 @@
* @param provider the location provider to query
* @param userId the userId to query
* @return true if the provider is enabled
- * @deprecated use {@link #LOCATION_MODE} or
- * {@link LocationManager#isProviderEnabled(String)}
+ *
+ * @deprecated use {@link LocationManager#isProviderEnabled(String)}
* @hide
*/
@Deprecated
- public static final boolean isLocationProviderEnabledForUser(ContentResolver cr, String provider, int userId) {
+ public static final boolean isLocationProviderEnabledForUser(
+ ContentResolver cr, String provider, int userId) {
String allowedProviders = Settings.Secure.getStringForUser(cr,
LOCATION_PROVIDERS_ALLOWED, userId);
return TextUtils.delimitedStringContains(allowedProviders, ',', provider);
@@ -7479,7 +7962,8 @@
* @param cr the content resolver to use
* @param provider the location provider to enable or disable
* @param enabled true if the provider should be enabled
- * @deprecated use {@link #putInt(ContentResolver, String, int)} and {@link #LOCATION_MODE}
+ * @deprecated This API is deprecated. It requires WRITE_SECURE_SETTINGS permission to
+ * change location settings.
*/
@Deprecated
public static final void setLocationProviderEnabled(ContentResolver cr,
@@ -7495,8 +7979,8 @@
* @param enabled true if the provider should be enabled
* @param userId the userId for which to enable/disable providers
* @return true if the value was set, false on database errors
- * @deprecated use {@link #putIntForUser(ContentResolver, String, int, int)} and
- * {@link #LOCATION_MODE}
+ *
+ * @deprecated use {@link LocationManager#setProviderEnabledForUser(String, boolean, int)}
* @hide
*/
@Deprecated
@@ -7517,28 +8001,6 @@
}
/**
- * Saves the current location mode into {@link #LOCATION_PREVIOUS_MODE}.
- */
- private static final boolean saveLocationModeForUser(ContentResolver cr, int userId) {
- final int mode = getLocationModeForUser(cr, userId);
- return putIntForUser(cr, Settings.Secure.LOCATION_PREVIOUS_MODE, mode, userId);
- }
-
- /**
- * Restores the current location mode from {@link #LOCATION_PREVIOUS_MODE}.
- */
- private static final boolean restoreLocationModeForUser(ContentResolver cr, int userId) {
- int mode = getIntForUser(cr, Settings.Secure.LOCATION_PREVIOUS_MODE,
- LOCATION_MODE_HIGH_ACCURACY, userId);
- // Make sure that the previous mode is never "off". Otherwise the user won't be able to
- // turn on location any longer.
- if (mode == LOCATION_MODE_OFF) {
- mode = LOCATION_MODE_HIGH_ACCURACY;
- }
- return setLocationModeForUser(cr, mode, userId);
- }
-
- /**
* Thread-safe method for setting the location mode to one of
* {@link #LOCATION_MODE_HIGH_ACCURACY}, {@link #LOCATION_MODE_SENSORS_ONLY},
* {@link #LOCATION_MODE_BATTERY_SAVING}, or {@link #LOCATION_MODE_OFF}.
@@ -7551,18 +8013,20 @@
* @return true if the value was set, false on database errors
*
* @throws IllegalArgumentException if mode is not one of the supported values
+ *
+ * @deprecated To enable/disable location, use
+ * {@link LocationManager#setLocationEnabledForUser(boolean, int)}.
+ * To enable/disable a specific location provider, use
+ * {@link LocationManager#setProviderEnabledForUser(String, boolean, int)}.
*/
- private static final boolean setLocationModeForUser(ContentResolver cr, int mode,
- int userId) {
+ @Deprecated
+ private static boolean setLocationModeForUser(
+ ContentResolver cr, int mode, int userId) {
synchronized (mLocationSettingsLock) {
boolean gps = false;
boolean network = false;
switch (mode) {
- case LOCATION_MODE_PREVIOUS:
- // Retrieve the actual mode and set to that mode.
- return restoreLocationModeForUser(cr, userId);
case LOCATION_MODE_OFF:
- saveLocationModeForUser(cr, userId);
break;
case LOCATION_MODE_SENSORS_ONLY:
gps = true;
@@ -7577,15 +8041,7 @@
default:
throw new IllegalArgumentException("Invalid location mode: " + mode);
}
- // Note it's important that we set the NLP mode first. The Google implementation
- // of NLP clears its NLP consent setting any time it receives a
- // LocationManager.PROVIDERS_CHANGED_ACTION broadcast and NLP is disabled. Also,
- // it shows an NLP consent dialog any time it receives the broadcast, NLP is
- // enabled, and the NLP consent is not set. If 1) we were to enable GPS first,
- // 2) a setup wizard has its own NLP consent UI that sets the NLP consent setting,
- // and 3) the receiver happened to complete before we enabled NLP, then the Google
- // NLP would detect the attempt to enable NLP and show a redundant NLP consent
- // dialog. Then the people who wrote the setup wizard would be sad.
+
boolean nlpSuccess = Settings.Secure.setLocationProviderEnabledForUser(
cr, LocationManager.NETWORK_PROVIDER, network, userId);
boolean gpsSuccess = Settings.Secure.setLocationProviderEnabledForUser(
@@ -7772,12 +8228,16 @@
*/
public static final String AUTO_TIME = "auto_time";
+ private static final Validator AUTO_TIME_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Value to specify if the user prefers the time zone
* to be automatically fetched from the network (NITZ). 1=yes, 0=no
*/
public static final String AUTO_TIME_ZONE = "auto_time_zone";
+ private static final Validator AUTO_TIME_ZONE_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* URI for the car dock "in" event sound.
* @hide
@@ -7808,6 +8268,8 @@
*/
public static final String DOCK_SOUNDS_ENABLED = "dock_sounds_enabled";
+ private static final Validator DOCK_SOUNDS_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether to play a sound for dock events, only when an accessibility service is on.
* @hide
@@ -7845,6 +8307,8 @@
*/
public static final String POWER_SOUNDS_ENABLED = "power_sounds_enabled";
+ private static final Validator POWER_SOUNDS_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* URI for the "wireless charging started" sound.
* @hide
@@ -7858,6 +8322,8 @@
*/
public static final String CHARGING_SOUNDS_ENABLED = "charging_sounds_enabled";
+ private static final Validator CHARGING_SOUNDS_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether we keep the device on while the device is plugged in.
* Supported values are:
@@ -7871,6 +8337,30 @@
*/
public static final String STAY_ON_WHILE_PLUGGED_IN = "stay_on_while_plugged_in";
+ private static final Validator STAY_ON_WHILE_PLUGGED_IN_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ try {
+ int val = Integer.parseInt(value);
+ return (val == 0)
+ || (val == BatteryManager.BATTERY_PLUGGED_AC)
+ || (val == BatteryManager.BATTERY_PLUGGED_USB)
+ || (val == BatteryManager.BATTERY_PLUGGED_WIRELESS)
+ || (val == (BatteryManager.BATTERY_PLUGGED_AC
+ | BatteryManager.BATTERY_PLUGGED_USB))
+ || (val == (BatteryManager.BATTERY_PLUGGED_AC
+ | BatteryManager.BATTERY_PLUGGED_WIRELESS))
+ || (val == (BatteryManager.BATTERY_PLUGGED_USB
+ | BatteryManager.BATTERY_PLUGGED_WIRELESS))
+ || (val == (BatteryManager.BATTERY_PLUGGED_AC
+ | BatteryManager.BATTERY_PLUGGED_USB
+ | BatteryManager.BATTERY_PLUGGED_WIRELESS));
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+ };
+
/**
* When the user has enable the option to have a "bug report" command
* in the power menu.
@@ -7878,6 +8368,8 @@
*/
public static final String BUGREPORT_IN_POWER_MENU = "bugreport_in_power_menu";
+ private static final Validator BUGREPORT_IN_POWER_MENU_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether ADB is enabled.
*/
@@ -7901,6 +8393,8 @@
*/
public static final String BLUETOOTH_ON = "bluetooth_on";
+ private static final Validator BLUETOOTH_ON_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* CDMA Cell Broadcast SMS
* 0 = CDMA Cell Broadcast SMS disabled
@@ -8519,6 +9013,8 @@
*/
public static final String USB_MASS_STORAGE_ENABLED = "usb_mass_storage_enabled";
+ private static final Validator USB_MASS_STORAGE_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* If this setting is set (to anything), then all references
* to Gmail on the device must change to Google Mail.
@@ -8655,6 +9151,25 @@
public static final String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON =
"wifi_networks_available_notification_on";
+ private static final Validator WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
+ /**
+ * Whether to notify the user of carrier networks.
+ * <p>
+ * If not connected and the scan results have a carrier network, we will
+ * put this notification up. If we attempt to connect to a network or
+ * the carrier network(s) disappear, we remove the notification. When we
+ * show the notification, we will not show it again for
+ * {@link android.provider.Settings.Global#WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY} time.
+ * @hide
+ */
+ public static final String WIFI_CARRIER_NETWORKS_AVAILABLE_NOTIFICATION_ON =
+ "wifi_carrier_networks_available_notification_on";
+
+ private static final Validator WIFI_CARRIER_NETWORKS_AVAILABLE_NOTIFICATION_ON_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* {@hide}
*/
@@ -8668,6 +9183,9 @@
public static final String WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY =
"wifi_networks_available_repeat_delay";
+ private static final Validator WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* 802.11 country code in ISO 3166 format
* @hide
@@ -8697,6 +9215,9 @@
*/
public static final String WIFI_NUM_OPEN_NETWORKS_KEPT = "wifi_num_open_networks_kept";
+ private static final Validator WIFI_NUM_OPEN_NETWORKS_KEPT_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* Whether the Wi-Fi should be on. Only the Wi-Fi service should touch this.
*/
@@ -8711,10 +9232,14 @@
/**
* Whether soft AP will shut down after a timeout period when no devices are connected.
+ *
+ * Type: int (0 for false, 1 for true)
* @hide
*/
public static final String SOFT_AP_TIMEOUT_ENABLED = "soft_ap_timeout_enabled";
+ private static final Validator SOFT_AP_TIMEOUT_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Value to specify if Wi-Fi Wakeup feature is enabled.
*
@@ -8724,6 +9249,8 @@
@SystemApi
public static final String WIFI_WAKEUP_ENABLED = "wifi_wakeup_enabled";
+ private static final Validator WIFI_WAKEUP_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Value to specify if Wi-Fi Wakeup is available.
*
@@ -8770,6 +9297,9 @@
public static final String NETWORK_RECOMMENDATIONS_ENABLED =
"network_recommendations_enabled";
+ private static final Validator NETWORK_RECOMMENDATIONS_ENABLED_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[] {"-1", "0", "1"});
+
/**
* Which package name to use for network recommendations. If null, network recommendations
* will neither be requested nor accepted.
@@ -8790,8 +9320,16 @@
* Type: string package name or null if the feature is either not provided or disabled.
* @hide
*/
+ @TestApi
public static final String USE_OPEN_WIFI_PACKAGE = "use_open_wifi_package";
+ private static final Validator USE_OPEN_WIFI_PACKAGE_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ return (value == null) || PACKAGE_NAME_VALIDATOR.validate(value);
+ }
+ };
+
/**
* The number of milliseconds the {@link com.android.server.NetworkScoreService}
* will give a recommendation request to complete before returning a default response.
@@ -8813,13 +9351,52 @@
public static final String RECOMMENDED_NETWORK_EVALUATOR_CACHE_EXPIRY_MS =
"recommended_network_evaluator_cache_expiry_ms";
- /**
+ /**
* Settings to allow BLE scans to be enabled even when Bluetooth is turned off for
* connectivity.
* @hide
*/
- public static final String BLE_SCAN_ALWAYS_AVAILABLE =
- "ble_scan_always_enabled";
+ public static final String BLE_SCAN_ALWAYS_AVAILABLE = "ble_scan_always_enabled";
+
+ /**
+ * The length in milliseconds of a BLE scan window in a low-power scan mode.
+ * @hide
+ */
+ public static final String BLE_SCAN_LOW_POWER_WINDOW_MS = "ble_scan_low_power_window_ms";
+
+ /**
+ * The length in milliseconds of a BLE scan window in a balanced scan mode.
+ * @hide
+ */
+ public static final String BLE_SCAN_BALANCED_WINDOW_MS = "ble_scan_balanced_window_ms";
+
+ /**
+ * The length in milliseconds of a BLE scan window in a low-latency scan mode.
+ * @hide
+ */
+ public static final String BLE_SCAN_LOW_LATENCY_WINDOW_MS =
+ "ble_scan_low_latency_window_ms";
+
+ /**
+ * The length in milliseconds of a BLE scan interval in a low-power scan mode.
+ * @hide
+ */
+ public static final String BLE_SCAN_LOW_POWER_INTERVAL_MS =
+ "ble_scan_low_power_interval_ms";
+
+ /**
+ * The length in milliseconds of a BLE scan interval in a balanced scan mode.
+ * @hide
+ */
+ public static final String BLE_SCAN_BALANCED_INTERVAL_MS =
+ "ble_scan_balanced_interval_ms";
+
+ /**
+ * The length in milliseconds of a BLE scan interval in a low-latency scan mode.
+ * @hide
+ */
+ public static final String BLE_SCAN_LOW_LATENCY_INTERVAL_MS =
+ "ble_scan_low_latency_interval_ms";
/**
* Used to save the Wifi_ON state prior to tethering.
@@ -8871,6 +9448,9 @@
public static final String WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED =
"wifi_watchdog_poor_network_test_enabled";
+ private static final Validator WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED_VALIDATOR =
+ ANY_STRING_VALIDATOR;
+
/**
* Setting to turn on suspend optimizations at screen off on Wi-Fi. Enabled by default and
* needs to be set to 0 to disable it.
@@ -8887,6 +9467,14 @@
public static final String WIFI_VERBOSE_LOGGING_ENABLED =
"wifi_verbose_logging_enabled";
+ /**
+ * Setting to enable connected MAC randomization in Wi-Fi; disabled by default, and
+ * setting to 1 will enable it. In the future, additional values may be supported.
+ * @hide
+ */
+ public static final String WIFI_CONNECTED_MAC_RANDOMIZATION_ENABLED =
+ "wifi_connected_mac_randomization_enabled";
+
/**
* The maximum number of times we will retry a connection to an access
* point for which we have failed in acquiring an IP address from DHCP.
@@ -9406,11 +9994,16 @@
* @hide
*/
public static final String PRIVATE_DNS_MODE = "private_dns_mode";
+
+ private static final Validator PRIVATE_DNS_MODE_VALIDATOR = ANY_STRING_VALIDATOR;
+
/**
* @hide
*/
public static final String PRIVATE_DNS_SPECIFIER = "private_dns_specifier";
+ private static final Validator PRIVATE_DNS_SPECIFIER_VALIDATOR = ANY_STRING_VALIDATOR;
+
/** {@hide} */
public static final String
BLUETOOTH_HEADSET_PRIORITY_PREFIX = "bluetooth_headset_priority_";
@@ -9586,7 +10179,8 @@
* This is encoded as a key=value list, separated by commas. Ex:
*
* "battery_tip_enabled=true,summary_enabled=true,high_usage_enabled=true,"
- * "high_usage_app_count=3,reduced_battery_enabled=false,reduced_battery_percent=50"
+ * "high_usage_app_count=3,reduced_battery_enabled=false,reduced_battery_percent=50,"
+ * "high_usage_battery_draining=25,high_usage_period_ms=3000"
*
* The following keys are supported:
*
@@ -9596,6 +10190,8 @@
* battery_saver_tip_enabled (boolean)
* high_usage_enabled (boolean)
* high_usage_app_count (int)
+ * high_usage_period_ms (long)
+ * high_usage_battery_draining (int)
* app_restriction_enabled (boolean)
* reduced_battery_enabled (boolean)
* reduced_battery_percent (int)
@@ -9626,6 +10222,25 @@
public static final String ALWAYS_ON_DISPLAY_CONSTANTS = "always_on_display_constants";
/**
+ * System VDSO global setting. This links to the "sys.vdso" system property.
+ * The following values are supported:
+ * false -> both 32 and 64 bit vdso disabled
+ * 32 -> 32 bit vdso enabled
+ * 64 -> 64 bit vdso enabled
+ * Any other value defaults to both 32 bit and 64 bit true.
+ * @hide
+ */
+ public static final String SYS_VDSO = "sys_vdso";
+
+ /**
+ * An integer to reduce the FPS by this factor. Only for experiments. Need to reboot the
+ * device for this setting to take full effect.
+ *
+ * @hide
+ */
+ public static final String FPS_DEVISOR = "fps_divisor";
+
+ /**
* App standby (app idle) specific settings.
* This is encoded as a key=value list, separated by commas. Ex:
* <p>
@@ -9781,6 +10396,24 @@
public static final String TEXT_CLASSIFIER_CONSTANTS = "text_classifier_constants";
/**
+ * BatteryStats specific settings.
+ * This is encoded as a key=value list, separated by commas. Ex: "foo=1,bar=true"
+ *
+ * The following keys are supported:
+ * <pre>
+ * track_cpu_times_by_proc_state (boolean)
+ * track_cpu_active_cluster_time (boolean)
+ * read_binary_cpu_time (boolean)
+ * </pre>
+ *
+ * <p>
+ * Type: string
+ * @hide
+ * see also com.android.internal.os.BatteryStatsImpl.Constants
+ */
+ public static final String BATTERY_STATS_CONSTANTS = "battery_stats_constants";
+
+ /**
* Whether or not App Standby feature is enabled. This controls throttling of apps
* based on usage patterns and predictions.
* Type: int (0 for false, 1 for true)
@@ -9790,6 +10423,31 @@
public static final java.lang.String APP_STANDBY_ENABLED = "app_standby_enabled";
/**
+ * Feature flag to enable or disable the Forced App Standby feature.
+ * Type: int (0 for false, 1 for true)
+ * Default: 1
+ * @hide
+ */
+ public static final String FORCED_APP_STANDBY_ENABLED = "forced_app_standby_enabled";
+
+ /**
+ * Whether or not to enable Forced App Standby on small battery devices.
+ * Type: int (0 for false, 1 for true)
+ * Default: 0
+ * @hide
+ */
+ public static final String FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED
+ = "forced_app_standby_for_small_battery_enabled";
+
+ /**
+ * Whether or not Network Watchlist feature is enabled.
+ * Type: int (0 for false, 1 for true)
+ * Default: 0
+ * @hide
+ */
+ public static final String NETWORK_WATCHLIST_ENABLED = "network_watchlist_enabled";
+
+ /**
* Get the key that retrieves a bluetooth headset's priority.
* @hide
*/
@@ -9932,6 +10590,9 @@
*/
public static final String EMERGENCY_TONE = "emergency_tone";
+ private static final Validator EMERGENCY_TONE_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[] {"0", "1", "2"});
+
/**
* CDMA only settings
* Whether the auto retry is enabled. The value is
@@ -9940,6 +10601,8 @@
*/
public static final String CALL_AUTO_RETRY = "call_auto_retry";
+ private static final Validator CALL_AUTO_RETRY_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* A setting that can be read whether the emergency affordance is currently needed.
* The value is a boolean (1 or 0).
@@ -9999,6 +10662,7 @@
* If 1 low power mode is enabled.
* @hide
*/
+ @TestApi
public static final String LOW_POWER_MODE = "low_power";
/**
@@ -10008,6 +10672,9 @@
*/
public static final String LOW_POWER_MODE_TRIGGER_LEVEL = "low_power_trigger_level";
+ private static final Validator LOW_POWER_MODE_TRIGGER_LEVEL_VALIDATOR =
+ new SettingsValidators.InclusiveIntegerRangeValidator(0, 99);
+
/**
* If not 0, the activity manager will aggressively finish activities and
* processes as soon as they are no longer needed. If 0, the normal
@@ -10023,6 +10690,8 @@
*/
public static final String DOCK_AUDIO_MEDIA_ENABLED = "dock_audio_media_enabled";
+ private static final Validator DOCK_AUDIO_MEDIA_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* The surround sound formats AC3, DTS or IEC61937 are
* available for use if they are detected.
@@ -10069,6 +10738,9 @@
*/
public static final String ENCODED_SURROUND_OUTPUT = "encoded_surround_output";
+ private static final Validator ENCODED_SURROUND_OUTPUT_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[] {"0", "1", "2"});
+
/**
* Persisted safe headphone volume management state by AudioService
* @hide
@@ -10564,10 +11236,20 @@
*
* @hide
*/
+ @TestApi
public static final String LOCATION_GLOBAL_KILL_SWITCH =
"location_global_kill_switch";
/**
+ * If set to 1, SettingsProvider's restoreAnyVersion="true" attribute will be ignored
+ * and restoring to lower version of platform API will be skipped.
+ *
+ * @hide
+ */
+ public static final String OVERRIDE_SETTINGS_PROVIDER_RESTORE_ANY_VERSION =
+ "override_settings_provider_restore_any_version";
+
+ /**
* Settings to backup. This is here so that it's in the same place as the settings
* keys and easy to update.
*
@@ -10595,6 +11277,7 @@
NETWORK_RECOMMENDATIONS_ENABLED,
WIFI_WAKEUP_ENABLED,
WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON,
+ WIFI_CARRIER_NETWORKS_AVAILABLE_NOTIFICATION_ON,
USE_OPEN_WIFI_PACKAGE,
WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED,
EMERGENCY_TONE,
@@ -10609,6 +11292,43 @@
};
/**
+ * All settings in {@link SETTINGS_TO_BACKUP} array *must* have a non-null validator,
+ * otherwise they won't be restored.
+ *
+ * @hide
+ */
+ public static final Map<String, Validator> VALIDATORS = new ArrayMap<>();
+ static {
+ VALIDATORS.put(BUGREPORT_IN_POWER_MENU, BUGREPORT_IN_POWER_MENU_VALIDATOR);
+ VALIDATORS.put(STAY_ON_WHILE_PLUGGED_IN, STAY_ON_WHILE_PLUGGED_IN_VALIDATOR);
+ VALIDATORS.put(AUTO_TIME, AUTO_TIME_VALIDATOR);
+ VALIDATORS.put(AUTO_TIME_ZONE, AUTO_TIME_ZONE_VALIDATOR);
+ VALIDATORS.put(POWER_SOUNDS_ENABLED, POWER_SOUNDS_ENABLED_VALIDATOR);
+ VALIDATORS.put(DOCK_SOUNDS_ENABLED, DOCK_SOUNDS_ENABLED_VALIDATOR);
+ VALIDATORS.put(CHARGING_SOUNDS_ENABLED, CHARGING_SOUNDS_ENABLED_VALIDATOR);
+ VALIDATORS.put(USB_MASS_STORAGE_ENABLED, USB_MASS_STORAGE_ENABLED_VALIDATOR);
+ VALIDATORS.put(NETWORK_RECOMMENDATIONS_ENABLED,
+ NETWORK_RECOMMENDATIONS_ENABLED_VALIDATOR);
+ VALIDATORS.put(WIFI_WAKEUP_ENABLED, WIFI_WAKEUP_ENABLED_VALIDATOR);
+ VALIDATORS.put(WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON,
+ WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON_VALIDATOR);
+ VALIDATORS.put(USE_OPEN_WIFI_PACKAGE, USE_OPEN_WIFI_PACKAGE_VALIDATOR);
+ VALIDATORS.put(WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED,
+ WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED_VALIDATOR);
+ VALIDATORS.put(EMERGENCY_TONE, EMERGENCY_TONE_VALIDATOR);
+ VALIDATORS.put(CALL_AUTO_RETRY, CALL_AUTO_RETRY_VALIDATOR);
+ VALIDATORS.put(DOCK_AUDIO_MEDIA_ENABLED, DOCK_AUDIO_MEDIA_ENABLED_VALIDATOR);
+ VALIDATORS.put(ENCODED_SURROUND_OUTPUT, ENCODED_SURROUND_OUTPUT_VALIDATOR);
+ VALIDATORS.put(LOW_POWER_MODE_TRIGGER_LEVEL, LOW_POWER_MODE_TRIGGER_LEVEL_VALIDATOR);
+ VALIDATORS.put(BLUETOOTH_ON, BLUETOOTH_ON_VALIDATOR);
+ VALIDATORS.put(PRIVATE_DNS_MODE, PRIVATE_DNS_MODE_VALIDATOR);
+ VALIDATORS.put(PRIVATE_DNS_SPECIFIER, PRIVATE_DNS_SPECIFIER_VALIDATOR);
+ VALIDATORS.put(SOFT_AP_TIMEOUT_ENABLED, SOFT_AP_TIMEOUT_ENABLED_VALIDATOR);
+ VALIDATORS.put(WIFI_CARRIER_NETWORKS_AVAILABLE_NOTIFICATION_ON,
+ WIFI_CARRIER_NETWORKS_AVAILABLE_NOTIFICATION_ON_VALIDATOR);
+ }
+
+ /**
* Global settings that shouldn't be persisted.
*
* @hide
@@ -10617,7 +11337,15 @@
LOCATION_GLOBAL_KILL_SWITCH,
};
- /** @hide */
+ /**
+ * Keys we no longer back up under the current schema, but want to continue to
+ * process when restoring historical backup datasets.
+ *
+ * All settings in {@link LEGACY_RESTORE_SETTINGS} array *must* have a non-null validator,
+ * otherwise they won't be restored.
+ *
+ * @hide
+ */
public static final String[] LEGACY_RESTORE_SETTINGS = {
};
@@ -11220,6 +11948,42 @@
*/
public static final String ENABLE_GNSS_RAW_MEAS_FULL_TRACKING =
"enable_gnss_raw_meas_full_tracking";
+
+ /**
+ * Whether we've enabled zram on this device. Takes effect on
+ * reboot. The value "1" enables zram; "0" disables it, and
+ * everything else is unspecified.
+ * @hide
+ */
+ public static final String ZRAM_ENABLED =
+ "zram_enabled";
+
+ /**
+ * Whether smart replies in notifications are enabled.
+ * @hide
+ */
+ public static final String ENABLE_SMART_REPLIES_IN_NOTIFICATIONS =
+ "enable_smart_replies_in_notifications";
+
+ /**
+ * If nonzero, crashes in foreground processes will bring up a dialog.
+ * Otherwise, the process will be silently killed.
+ * @hide
+ */
+ public static final String SHOW_FIRST_CRASH_DIALOG = "show_first_crash_dialog";
+
+ /**
+ * If nonzero, crash dialogs will show an option to restart the app.
+ * @hide
+ */
+ public static final String SHOW_RESTART_IN_CRASH_DIALOG = "show_restart_in_crash_dialog";
+
+ /**
+ * If nonzero, crash dialogs will show an option to mute all future crash dialogs for
+ * this app.
+ * @hide
+ */
+ public static final String SHOW_MUTE_IN_CRASH_DIALOG = "show_mute_in_crash_dialog";
}
/**
diff --git a/android/provider/SettingsValidators.java b/android/provider/SettingsValidators.java
new file mode 100644
index 0000000..5885b6b
--- /dev/null
+++ b/android/provider/SettingsValidators.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2018 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.provider;
+
+import android.content.ComponentName;
+import android.net.Uri;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.util.Locale;
+
+/**
+ * This class provides both interface for validation and common validators
+ * used to ensure Settings have meaningful values.
+ *
+ * @hide
+ */
+public class SettingsValidators {
+
+ public static final Validator BOOLEAN_VALIDATOR =
+ new DiscreteValueValidator(new String[] {"0", "1"});
+
+ public static final Validator ANY_STRING_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ return true;
+ }
+ };
+
+ public static final Validator NON_NEGATIVE_INTEGER_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ try {
+ return Integer.parseInt(value) >= 0;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+ };
+
+ public static final Validator ANY_INTEGER_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ try {
+ Integer.parseInt(value);
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+ };
+
+ public static final Validator URI_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ try {
+ Uri.decode(value);
+ return true;
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+ };
+
+ public static final Validator COMPONENT_NAME_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ return ComponentName.unflattenFromString(value) != null;
+ }
+ };
+
+ public static final Validator PACKAGE_NAME_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ return value != null && isStringPackageName(value);
+ }
+
+ private boolean isStringPackageName(String value) {
+ // The name may contain uppercase or lowercase letters ('A' through 'Z'), numbers,
+ // and underscores ('_'). However, individual package name parts may only
+ // start with letters.
+ // (https://developer.android.com/guide/topics/manifest/manifest-element.html#package)
+ if (value == null) {
+ return false;
+ }
+ String[] subparts = value.split("\\.");
+ boolean isValidPackageName = true;
+ for (String subpart : subparts) {
+ isValidPackageName &= isSubpartValidForPackageName(subpart);
+ if (!isValidPackageName) break;
+ }
+ return isValidPackageName;
+ }
+
+ private boolean isSubpartValidForPackageName(String subpart) {
+ if (subpart.length() == 0) return false;
+ boolean isValidSubpart = Character.isLetter(subpart.charAt(0));
+ for (int i = 1; i < subpart.length(); i++) {
+ isValidSubpart &= (Character.isLetterOrDigit(subpart.charAt(i))
+ || (subpart.charAt(i) == '_'));
+ if (!isValidSubpart) break;
+ }
+ return isValidSubpart;
+ }
+ };
+
+ public static final Validator LENIENT_IP_ADDRESS_VALIDATOR = new Validator() {
+ private static final int MAX_IPV6_LENGTH = 45;
+
+ @Override
+ public boolean validate(String value) {
+ if (value == null) {
+ return false;
+ }
+ return value.length() <= MAX_IPV6_LENGTH;
+ }
+ };
+
+ public static final Validator LOCALE_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ if (value == null) {
+ return false;
+ }
+ Locale[] validLocales = Locale.getAvailableLocales();
+ for (Locale locale : validLocales) {
+ if (value.equals(locale.toString())) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+
+ public interface Validator {
+ boolean validate(String value);
+ }
+
+ public static final class DiscreteValueValidator implements Validator {
+ private final String[] mValues;
+
+ public DiscreteValueValidator(String[] values) {
+ mValues = values;
+ }
+
+ @Override
+ public boolean validate(String value) {
+ return ArrayUtils.contains(mValues, value);
+ }
+ }
+
+ public static final class InclusiveIntegerRangeValidator implements Validator {
+ private final int mMin;
+ private final int mMax;
+
+ public InclusiveIntegerRangeValidator(int min, int max) {
+ mMin = min;
+ mMax = max;
+ }
+
+ @Override
+ public boolean validate(String value) {
+ try {
+ final int intValue = Integer.parseInt(value);
+ return intValue >= mMin && intValue <= mMax;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+ }
+
+ public static final class InclusiveFloatRangeValidator implements Validator {
+ private final float mMin;
+ private final float mMax;
+
+ public InclusiveFloatRangeValidator(float min, float max) {
+ mMin = min;
+ mMax = max;
+ }
+
+ @Override
+ public boolean validate(String value) {
+ try {
+ final float floatValue = Float.parseFloat(value);
+ return floatValue >= mMin && floatValue <= mMax;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+ }
+
+ public static final class ComponentNameListValidator implements Validator {
+ private final String mSeparator;
+
+ public ComponentNameListValidator(String separator) {
+ mSeparator = separator;
+ }
+
+ @Override
+ public boolean validate(String value) {
+ if (value == null) {
+ return false;
+ }
+ String[] elements = value.split(mSeparator);
+ for (String element : elements) {
+ if (!COMPONENT_NAME_VALIDATOR.validate(element)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ public static final class PackageNameListValidator implements Validator {
+ private final String mSeparator;
+
+ public PackageNameListValidator(String separator) {
+ mSeparator = separator;
+ }
+
+ @Override
+ public boolean validate(String value) {
+ if (value == null) {
+ return false;
+ }
+ String[] elements = value.split(mSeparator);
+ for (String element : elements) {
+ if (!PACKAGE_NAME_VALIDATOR.validate(element)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/android/provider/Telephony.java b/android/provider/Telephony.java
index 942ea00..8c45724 100644
--- a/android/provider/Telephony.java
+++ b/android/provider/Telephony.java
@@ -1102,6 +1102,16 @@
"android.provider.Telephony.MMS_DOWNLOADED";
/**
+ * Broadcast Action: A debug code has been entered in the dialer. These "secret codes"
+ * are used to activate developer menus by dialing certain codes. And they are of the
+ * form {@code *#*#<code>#*#*}. The intent will have the data URI:
+ * {@code android_secret_code://<code>}.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String SECRET_CODE_ACTION =
+ "android.provider.Telephony.SECRET_CODE";
+
+ /**
* Broadcast action: When the default SMS package changes,
* the previous default SMS package and the new default SMS
* package are sent this broadcast to notify them of the change.
@@ -2564,6 +2574,35 @@
public static final Uri CONTENT_URI = Uri.parse("content://telephony/carriers");
/**
+ * The {@code content://} style URL to be called from DevicePolicyManagerService,
+ * can manage DPC-owned APNs.
+ * @hide
+ */
+ public static final Uri DPC_URI = Uri.parse("content://telephony/carriers/dpc");
+
+ /**
+ * The {@code content://} style URL to be called from Telephony to query APNs.
+ * When DPC-owned APNs are enforced, only DPC-owned APNs are returned, otherwise only
+ * non-DPC-owned APNs are returned.
+ * @hide
+ */
+ public static final Uri FILTERED_URI = Uri.parse("content://telephony/carriers/filtered");
+
+ /**
+ * The {@code content://} style URL to be called from DevicePolicyManagerService
+ * or Telephony to manage whether DPC-owned APNs are enforced.
+ * @hide
+ */
+ public static final Uri ENFORCE_MANAGED_URI = Uri.parse(
+ "content://telephony/carriers/enforce_managed");
+
+ /**
+ * The column name for ENFORCE_MANAGED_URI, indicates whether DPC-owned APNs are enforced.
+ * @hide
+ */
+ public static final String ENFORCE_KEY = "enforced";
+
+ /**
* The default sort order for this table.
*/
public static final String DEFAULT_SORT_ORDER = "name ASC";
@@ -2693,6 +2732,7 @@
* but is currently only used for LTE (14) and eHRPD (13).
* <P>Type: INTEGER</P>
*/
+ @Deprecated
public static final String BEARER = "bearer";
/**
@@ -2704,9 +2744,19 @@
* <P>Type: INTEGER</P>
* @hide
*/
+ @Deprecated
public static final String BEARER_BITMASK = "bearer_bitmask";
/**
+ * Radio technology (network type) bitmask.
+ * To check what values can be contained, refer to
+ * {@link android.telephony.TelephonyManager}.
+ * Bitmask for a radio tech R is (1 << (R - 1))
+ * <P>Type: INTEGER</P>
+ */
+ public static final String NETWORK_TYPE_BITMASK = "network_type_bitmask";
+
+ /**
* MVNO type:
* {@code SPN (Service Provider Name), IMSI, GID (Group Identifier Level 1)}.
* <P>Type: TEXT</P>
diff --git a/android/provider/VoicemailContract.java b/android/provider/VoicemailContract.java
index 6a3c55e..c568b6f 100644
--- a/android/provider/VoicemailContract.java
+++ b/android/provider/VoicemailContract.java
@@ -106,9 +106,12 @@
/**
* Broadcast intent to inform a new visual voicemail SMS has been received. This intent will
- * only be delivered to the telephony service. {@link #EXTRA_VOICEMAIL_SMS} will be included.
- */
- /** @hide */
+ * only be delivered to the telephony service.
+ *
+ * @see #EXTRA_VOICEMAIL_SMS
+ * @see #EXTRA_TARGET_PACKAGE
+ *
+ * @hide */
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
public static final String ACTION_VOICEMAIL_SMS_RECEIVED =
"com.android.internal.provider.action.VOICEMAIL_SMS_RECEIVED";
@@ -121,6 +124,19 @@
public static final String EXTRA_VOICEMAIL_SMS = "android.provider.extra.VOICEMAIL_SMS";
/**
+ * Extra in {@link #ACTION_VOICEMAIL_SMS_RECEIVED} indicating the target package to bind {@link
+ * android.telephony.VisualVoicemailService}.
+ *
+ * <p>This extra should be set to android.telephony.VisualVoicemailSmsFilterSettings#packageName
+ * while performing filtering. Since the default dialer might change between the filter sending
+ * it and telephony binding to the service, this ensures the service will not receive SMS
+ * filtered by the previous app.
+ *
+ * @hide
+ */
+ public static final String EXTRA_TARGET_PACKAGE = "android.provider.extra.TARGET_PACAKGE";
+
+ /**
* Extra included in {@link Intent#ACTION_PROVIDER_CHANGED} broadcast intents to indicate if the
* receiving package made this change.
*/
@@ -172,6 +188,11 @@
*/
public static final String DURATION = Calls.DURATION;
/**
+ * Whether or not the voicemail has been acknowledged (notification sent to the user).
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String NEW = Calls.NEW;
+ /**
* Whether this item has been read or otherwise consumed by the user.
* <P>Type: INTEGER (boolean)</P>
*/
diff --git a/android/security/KeyStore.java b/android/security/KeyStore.java
index fabcdf0..e25386b 100644
--- a/android/security/KeyStore.java
+++ b/android/security/KeyStore.java
@@ -424,15 +424,6 @@
return getmtime(key, UID_SELF);
}
- public boolean duplicate(String srcKey, int srcUid, String destKey, int destUid) {
- try {
- return mBinder.duplicate(srcKey, srcUid, destKey, destUid) == NO_ERROR;
- } catch (RemoteException e) {
- Log.w(TAG, "Cannot connect to keystore", e);
- return false;
- }
- }
-
// TODO: remove this when it's removed from Settings
public boolean isHardwareBacked() {
return isHardwareBacked("RSA");
@@ -519,6 +510,19 @@
return importKey(alias, args, format, keyData, UID_SELF, flags, outCharacteristics);
}
+ public int importWrappedKey(String wrappedKeyAlias, byte[] wrappedKey,
+ String wrappingKeyAlias,
+ byte[] maskingKey, KeymasterArguments args, long rootSid, long fingerprintSid, int uid,
+ KeyCharacteristics outCharacteristics) {
+ try {
+ return mBinder.importWrappedKey(wrappedKeyAlias, wrappedKey, wrappingKeyAlias,
+ maskingKey, args, rootSid, fingerprintSid, outCharacteristics);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Cannot connect to keystore", e);
+ return SYSTEM_ERROR;
+ }
+ }
+
public ExportResult exportKey(String alias, int format, KeymasterBlob clientId,
KeymasterBlob appId, int uid) {
try {
diff --git a/android/security/keymaster/KeymasterDefs.java b/android/security/keymaster/KeymasterDefs.java
index f409e5b..3464370 100644
--- a/android/security/keymaster/KeymasterDefs.java
+++ b/android/security/keymaster/KeymasterDefs.java
@@ -73,6 +73,7 @@
public static final int KM_TAG_USER_AUTH_TYPE = KM_ENUM | 504;
public static final int KM_TAG_AUTH_TIMEOUT = KM_UINT | 505;
public static final int KM_TAG_ALLOW_WHILE_ON_BODY = KM_BOOL | 506;
+ public static final int KM_TAG_TRUSTED_USER_PRESENCE_REQUIRED = KM_BOOL | 507;
public static final int KM_TAG_ALL_APPLICATIONS = KM_BOOL | 600;
public static final int KM_TAG_APPLICATION_ID = KM_BYTES | 601;
@@ -101,6 +102,7 @@
public static final int KM_ALGORITHM_RSA = 1;
public static final int KM_ALGORITHM_EC = 3;
public static final int KM_ALGORITHM_AES = 32;
+ public static final int KM_ALGORITHM_3DES = 33;
public static final int KM_ALGORITHM_HMAC = 128;
// Block modes.
@@ -130,6 +132,7 @@
public static final int KM_ORIGIN_GENERATED = 0;
public static final int KM_ORIGIN_IMPORTED = 2;
public static final int KM_ORIGIN_UNKNOWN = 3;
+ public static final int KM_ORIGIN_SECURELY_IMPORTED = 4;
// Key usability requirements.
public static final int KM_BLOB_STANDALONE = 0;
@@ -140,6 +143,7 @@
public static final int KM_PURPOSE_DECRYPT = 1;
public static final int KM_PURPOSE_SIGN = 2;
public static final int KM_PURPOSE_VERIFY = 3;
+ public static final int KM_PURPOSE_WRAP = 5;
// Key formats.
public static final int KM_KEY_FORMAT_X509 = 0;
diff --git a/android/security/keystore/AndroidKeyStore3DESCipherSpi.java b/android/security/keystore/AndroidKeyStore3DESCipherSpi.java
new file mode 100644
index 0000000..01fd062
--- /dev/null
+++ b/android/security/keystore/AndroidKeyStore3DESCipherSpi.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2018 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.security.keystore;
+
+import android.security.keymaster.KeymasterArguments;
+import android.security.keymaster.KeymasterDefs;
+
+import java.security.AlgorithmParameters;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.security.ProviderException;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.InvalidParameterSpecException;
+import java.util.Arrays;
+
+import javax.crypto.CipherSpi;
+import javax.crypto.spec.IvParameterSpec;
+
+/**
+ * Base class for Android Keystore 3DES {@link CipherSpi} implementations.
+ *
+ * @hide
+ */
+public class AndroidKeyStore3DESCipherSpi extends AndroidKeyStoreCipherSpiBase {
+
+ private static final int BLOCK_SIZE_BYTES = 8;
+
+ private final int mKeymasterBlockMode;
+ private final int mKeymasterPadding;
+ /** Whether this transformation requires an IV. */
+ private final boolean mIvRequired;
+
+ private byte[] mIv;
+
+ /** Whether the current {@code #mIv} has been used by the underlying crypto operation. */
+ private boolean mIvHasBeenUsed;
+
+ AndroidKeyStore3DESCipherSpi(
+ int keymasterBlockMode,
+ int keymasterPadding,
+ boolean ivRequired) {
+ mKeymasterBlockMode = keymasterBlockMode;
+ mKeymasterPadding = keymasterPadding;
+ mIvRequired = ivRequired;
+ }
+
+ abstract static class ECB extends AndroidKeyStore3DESCipherSpi {
+ protected ECB(int keymasterPadding) {
+ super(KeymasterDefs.KM_MODE_ECB, keymasterPadding, false);
+ }
+
+ public static class NoPadding extends ECB {
+ public NoPadding() {
+ super(KeymasterDefs.KM_PAD_NONE);
+ }
+ }
+
+ public static class PKCS7Padding extends ECB {
+ public PKCS7Padding() {
+ super(KeymasterDefs.KM_PAD_PKCS7);
+ }
+ }
+ }
+
+ abstract static class CBC extends AndroidKeyStore3DESCipherSpi {
+ protected CBC(int keymasterPadding) {
+ super(KeymasterDefs.KM_MODE_CBC, keymasterPadding, true);
+ }
+
+ public static class NoPadding extends CBC {
+ public NoPadding() {
+ super(KeymasterDefs.KM_PAD_NONE);
+ }
+ }
+
+ public static class PKCS7Padding extends CBC {
+ public PKCS7Padding() {
+ super(KeymasterDefs.KM_PAD_PKCS7);
+ }
+ }
+ }
+
+ @Override
+ protected void initKey(int i, Key key) throws InvalidKeyException {
+ if (!(key instanceof AndroidKeyStoreSecretKey)) {
+ throw new InvalidKeyException(
+ "Unsupported key: " + ((key != null) ? key.getClass().getName() : "null"));
+ }
+ if (!KeyProperties.KEY_ALGORITHM_3DES.equalsIgnoreCase(key.getAlgorithm())) {
+ throw new InvalidKeyException(
+ "Unsupported key algorithm: " + key.getAlgorithm() + ". Only " +
+ KeyProperties.KEY_ALGORITHM_3DES + " supported");
+ }
+ setKey((AndroidKeyStoreSecretKey) key);
+ }
+
+ @Override
+ protected int engineGetBlockSize() {
+ return BLOCK_SIZE_BYTES;
+ }
+
+ @Override
+ protected int engineGetOutputSize(int inputLen) {
+ return inputLen + 3 * BLOCK_SIZE_BYTES;
+ }
+
+ @Override
+ protected final byte[] engineGetIV() {
+ return ArrayUtils.cloneIfNotEmpty(mIv);
+ }
+
+ @Override
+ protected AlgorithmParameters engineGetParameters() {
+ if (!mIvRequired) {
+ return null;
+ }
+ if ((mIv != null) && (mIv.length > 0)) {
+ try {
+ AlgorithmParameters params = AlgorithmParameters.getInstance("DESede");
+ params.init(new IvParameterSpec(mIv));
+ return params;
+ } catch (NoSuchAlgorithmException e) {
+ throw new ProviderException(
+ "Failed to obtain 3DES AlgorithmParameters", e);
+ } catch (InvalidParameterSpecException e) {
+ throw new ProviderException(
+ "Failed to initialize 3DES AlgorithmParameters with an IV",
+ e);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void initAlgorithmSpecificParameters() throws InvalidKeyException {
+ if (!mIvRequired) {
+ return;
+ }
+
+ // IV is used
+ if (!isEncrypting()) {
+ throw new InvalidKeyException("IV required when decrypting"
+ + ". Use IvParameterSpec or AlgorithmParameters to provide it.");
+ }
+ }
+
+ @Override
+ protected void initAlgorithmSpecificParameters(AlgorithmParameterSpec params)
+ throws InvalidAlgorithmParameterException {
+ if (!mIvRequired) {
+ if (params != null) {
+ throw new InvalidAlgorithmParameterException("Unsupported parameters: " + params);
+ }
+ return;
+ }
+
+ // IV is used
+ if (params == null) {
+ if (!isEncrypting()) {
+ // IV must be provided by the caller
+ throw new InvalidAlgorithmParameterException(
+ "IvParameterSpec must be provided when decrypting");
+ }
+ return;
+ }
+ if (!(params instanceof IvParameterSpec)) {
+ throw new InvalidAlgorithmParameterException("Only IvParameterSpec supported");
+ }
+ mIv = ((IvParameterSpec) params).getIV();
+ if (mIv == null) {
+ throw new InvalidAlgorithmParameterException("Null IV in IvParameterSpec");
+ }
+ }
+
+ @Override
+ protected void initAlgorithmSpecificParameters(AlgorithmParameters params)
+ throws InvalidAlgorithmParameterException {
+ if (!mIvRequired) {
+ if (params != null) {
+ throw new InvalidAlgorithmParameterException("Unsupported parameters: " + params);
+ }
+ return;
+ }
+
+ // IV is used
+ if (params == null) {
+ if (!isEncrypting()) {
+ // IV must be provided by the caller
+ throw new InvalidAlgorithmParameterException("IV required when decrypting"
+ + ". Use IvParameterSpec or AlgorithmParameters to provide it.");
+ }
+ return;
+ }
+
+ if (!"DESede".equalsIgnoreCase(params.getAlgorithm())) {
+ throw new InvalidAlgorithmParameterException(
+ "Unsupported AlgorithmParameters algorithm: " + params.getAlgorithm()
+ + ". Supported: DESede");
+ }
+
+ IvParameterSpec ivSpec;
+ try {
+ ivSpec = params.getParameterSpec(IvParameterSpec.class);
+ } catch (InvalidParameterSpecException e) {
+ if (!isEncrypting()) {
+ // IV must be provided by the caller
+ throw new InvalidAlgorithmParameterException("IV required when decrypting"
+ + ", but not found in parameters: " + params, e);
+ }
+ mIv = null;
+ return;
+ }
+ mIv = ivSpec.getIV();
+ if (mIv == null) {
+ throw new InvalidAlgorithmParameterException("Null IV in AlgorithmParameters");
+ }
+ }
+
+ @Override
+ protected final int getAdditionalEntropyAmountForBegin() {
+ if ((mIvRequired) && (mIv == null) && (isEncrypting())) {
+ // IV will need to be generated
+ return BLOCK_SIZE_BYTES;
+ }
+
+ return 0;
+ }
+
+ @Override
+ protected int getAdditionalEntropyAmountForFinish() {
+ return 0;
+ }
+
+ @Override
+ protected void addAlgorithmSpecificParametersToBegin(KeymasterArguments keymasterArgs) {
+ if ((isEncrypting()) && (mIvRequired) && (mIvHasBeenUsed)) {
+ // IV is being reused for encryption: this violates security best practices.
+ throw new IllegalStateException(
+ "IV has already been used. Reusing IV in encryption mode violates security best"
+ + " practices.");
+ }
+
+ keymasterArgs.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_3DES);
+ keymasterArgs.addEnum(KeymasterDefs.KM_TAG_BLOCK_MODE, mKeymasterBlockMode);
+ keymasterArgs.addEnum(KeymasterDefs.KM_TAG_PADDING, mKeymasterPadding);
+ if ((mIvRequired) && (mIv != null)) {
+ keymasterArgs.addBytes(KeymasterDefs.KM_TAG_NONCE, mIv);
+ }
+ }
+
+ @Override
+ protected void loadAlgorithmSpecificParametersFromBeginResult(
+ KeymasterArguments keymasterArgs) {
+ mIvHasBeenUsed = true;
+
+ // NOTE: Keymaster doesn't always return an IV, even if it's used.
+ byte[] returnedIv = keymasterArgs.getBytes(KeymasterDefs.KM_TAG_NONCE, null);
+ if ((returnedIv != null) && (returnedIv.length == 0)) {
+ returnedIv = null;
+ }
+
+ if (mIvRequired) {
+ if (mIv == null) {
+ mIv = returnedIv;
+ } else if ((returnedIv != null) && (!Arrays.equals(returnedIv, mIv))) {
+ throw new ProviderException("IV in use differs from provided IV");
+ }
+ } else {
+ if (returnedIv != null) {
+ throw new ProviderException(
+ "IV in use despite IV not being used by this transformation");
+ }
+ }
+ }
+
+ @Override
+ protected final void resetAll() {
+ mIv = null;
+ mIvHasBeenUsed = false;
+ super.resetAll();
+ }
+}
diff --git a/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java b/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java
index be390ff..e4cf84a 100644
--- a/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java
+++ b/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java
@@ -93,6 +93,16 @@
putSymmetricCipherImpl("AES/CTR/NoPadding",
PACKAGE_NAME + ".AndroidKeyStoreUnauthenticatedAESCipherSpi$CTR$NoPadding");
+ putSymmetricCipherImpl("DESede/CBC/NoPadding",
+ PACKAGE_NAME + ".AndroidKeyStore3DESCipherSpi$CBC$NoPadding");
+ putSymmetricCipherImpl("DESede/CBC/PKCS7Padding",
+ PACKAGE_NAME + ".AndroidKeyStore3DESCipherSpi$CBC$PKCS7Padding");
+
+ putSymmetricCipherImpl("DESede/ECB/NoPadding",
+ PACKAGE_NAME + ".AndroidKeyStore3DESCipherSpi$ECB$NoPadding");
+ putSymmetricCipherImpl("DESede/ECB/PKCS7Padding",
+ PACKAGE_NAME + ".AndroidKeyStore3DESCipherSpi$ECB$PKCS7Padding");
+
putSymmetricCipherImpl("AES/GCM/NoPadding",
PACKAGE_NAME + ".AndroidKeyStoreAuthenticatedAESCipherSpi$GCM$NoPadding");
diff --git a/android/security/keystore/AndroidKeyStoreCipherSpiBase.java b/android/security/keystore/AndroidKeyStoreCipherSpiBase.java
index fdebf37..5bcb34a 100644
--- a/android/security/keystore/AndroidKeyStoreCipherSpiBase.java
+++ b/android/security/keystore/AndroidKeyStoreCipherSpiBase.java
@@ -307,7 +307,7 @@
*
* <p>This implementation returns {@code null}.
*
- * @returns stream or {@code null} if AAD is not supported by this cipher.
+ * @return stream or {@code null} if AAD is not supported by this cipher.
*/
@Nullable
protected KeyStoreCryptoOperationStreamer createAdditionalAuthenticationDataStreamer(
diff --git a/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java b/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java
index f1d1e16..379e177 100644
--- a/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java
+++ b/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java
@@ -60,6 +60,12 @@
}
}
+ public static class DESede extends AndroidKeyStoreKeyGeneratorSpi {
+ public DESede() {
+ super(KeymasterDefs.KM_ALGORITHM_3DES, 168);
+ }
+ }
+
protected static abstract class HmacBase extends AndroidKeyStoreKeyGeneratorSpi {
protected HmacBase(int keymasterDigest) {
super(KeymasterDefs.KM_ALGORITHM_HMAC,
diff --git a/android/security/keystore/AndroidKeyStoreProvider.java b/android/security/keystore/AndroidKeyStoreProvider.java
index 55e6519..1018926 100644
--- a/android/security/keystore/AndroidKeyStoreProvider.java
+++ b/android/security/keystore/AndroidKeyStoreProvider.java
@@ -80,6 +80,7 @@
// javax.crypto.KeyGenerator
put("KeyGenerator.AES", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$AES");
+ put("KeyGenerator.DESede", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$DESede");
put("KeyGenerator.HmacSHA1", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA1");
put("KeyGenerator.HmacSHA224", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA224");
put("KeyGenerator.HmacSHA256", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA256");
@@ -88,6 +89,7 @@
// java.security.SecretKeyFactory
putSecretKeyFactoryImpl("AES");
+ putSecretKeyFactoryImpl("DESede");
putSecretKeyFactoryImpl("HmacSHA1");
putSecretKeyFactoryImpl("HmacSHA224");
putSecretKeyFactoryImpl("HmacSHA256");
@@ -348,7 +350,8 @@
}
if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC ||
- keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_AES) {
+ keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_AES ||
+ keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_3DES) {
return loadAndroidKeyStoreSecretKeyFromKeystore(userKeyAlias, uid,
keyCharacteristics);
} else if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_RSA ||
diff --git a/android/security/keystore/AndroidKeyStoreSpi.java b/android/security/keystore/AndroidKeyStoreSpi.java
index d73a9e2..440e086 100644
--- a/android/security/keystore/AndroidKeyStoreSpi.java
+++ b/android/security/keystore/AndroidKeyStoreSpi.java
@@ -18,6 +18,7 @@
import libcore.util.EmptyArray;
import android.security.Credentials;
+import android.security.GateKeeper;
import android.security.KeyStore;
import android.security.KeyStoreParameter;
import android.security.keymaster.KeyCharacteristics;
@@ -25,6 +26,7 @@
import android.security.keymaster.KeymasterDefs;
import android.security.keystore.KeyProperties;
import android.security.keystore.KeyProtection;
+import android.security.keystore.WrappedKeyEntry;
import android.util.Log;
import java.io.ByteArrayInputStream;
@@ -744,6 +746,31 @@
}
}
+ private void setWrappedKeyEntry(String alias, byte[] wrappedKeyBytes, String wrappingKeyAlias,
+ java.security.KeyStore.ProtectionParameter param) throws KeyStoreException {
+ if (param != null) {
+ throw new KeyStoreException("Protection parameters are specified inside wrapped keys");
+ }
+
+ byte[] maskingKey = new byte[32];
+ KeymasterArguments args = new KeymasterArguments(); // TODO: populate wrapping key args.
+
+ int errorCode = mKeyStore.importWrappedKey(
+ Credentials.USER_SECRET_KEY + alias,
+ wrappedKeyBytes,
+ Credentials.USER_PRIVATE_KEY + wrappingKeyAlias,
+ maskingKey,
+ args,
+ GateKeeper.getSecureUserId(),
+ 0, // FIXME fingerprint id?
+ mUid,
+ new KeyCharacteristics());
+ if (errorCode != KeyStore.NO_ERROR) {
+ throw new KeyStoreException("Failed to import wrapped key. Keystore error code: "
+ + errorCode);
+ }
+ }
+
@Override
public void engineSetKeyEntry(String alias, byte[] userKey, Certificate[] chain)
throws KeyStoreException {
@@ -974,6 +1001,9 @@
} else if (entry instanceof SecretKeyEntry) {
SecretKeyEntry secE = (SecretKeyEntry) entry;
setSecretKeyEntry(alias, secE.getSecretKey(), param);
+ } else if (entry instanceof WrappedKeyEntry) {
+ WrappedKeyEntry wke = (WrappedKeyEntry) entry;
+ setWrappedKeyEntry(alias, wke.getWrappedKeyBytes(), wke.getWrappingKeyAlias(), param);
} else {
throw new KeyStoreException(
"Entry must be a PrivateKeyEntry, SecretKeyEntry or TrustedCertificateEntry"
diff --git a/android/security/keystore/AttestationUtils.java b/android/security/keystore/AttestationUtils.java
index 0811100..efee8b4 100644
--- a/android/security/keystore/AttestationUtils.java
+++ b/android/security/keystore/AttestationUtils.java
@@ -99,48 +99,35 @@
}
}
- /**
- * Performs attestation of the device's identifiers. This method returns a certificate chain
- * whose first element contains the requested device identifiers in an extension. The device's
- * manufacturer, model, brand, device and product are always also included in the attestation.
- * If the device supports attestation in secure hardware, the chain will be rooted at a
- * trustworthy CA key. Otherwise, the chain will be rooted at an untrusted certificate. See
- * <a href="https://developer.android.com/training/articles/security-key-attestation.html">
- * Key Attestation</a> for the format of the certificate extension.
- * <p>
- * Attestation will only be successful when all of the following are true:
- * 1) The device has been set up to support device identifier attestation at the factory.
- * 2) The user has not permanently disabled device identifier attestation.
- * 3) You have permission to access the device identifiers you are requesting attestation for.
- * <p>
- * For privacy reasons, you cannot distinguish between (1) and (2). If attestation is
- * unsuccessful, the device may not support it in general or the user may have permanently
- * disabled it.
- *
- * @param context the context to use for retrieving device identifiers.
- * @param idTypes the types of device identifiers to attest.
- * @param attestationChallenge a blob to include in the certificate alongside the device
- * identifiers.
- *
- * @return a certificate chain containing the requested device identifiers in the first element
- *
- * @exception SecurityException if you are not permitted to obtain an attestation of the
- * device's identifiers.
- * @exception DeviceIdAttestationException if the attestation operation fails.
- */
- @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
- @NonNull public static X509Certificate[] attestDeviceIds(Context context,
- @NonNull int[] idTypes, @NonNull byte[] attestationChallenge) throws
+ @NonNull private static KeymasterArguments prepareAttestationArgumentsForDeviceId(
+ Context context, @NonNull int[] idTypes, @NonNull byte[] attestationChallenge) throws
DeviceIdAttestationException {
- // Check method arguments, retrieve requested device IDs and prepare attestation arguments.
+ // Verify that device ID attestation types are provided.
if (idTypes == null) {
throw new NullPointerException("Missing id types");
}
+
+ return prepareAttestationArguments(context, idTypes, attestationChallenge);
+ }
+
+ /**
+ * Prepares Keymaster Arguments with attestation data.
+ * @hide should only be used by KeyChain.
+ */
+ @NonNull public static KeymasterArguments prepareAttestationArguments(Context context,
+ @NonNull int[] idTypes, @NonNull byte[] attestationChallenge) throws
+ DeviceIdAttestationException {
+ // Check method arguments, retrieve requested device IDs and prepare attestation arguments.
if (attestationChallenge == null) {
throw new NullPointerException("Missing attestation challenge");
}
final KeymasterArguments attestArgs = new KeymasterArguments();
attestArgs.addBytes(KeymasterDefs.KM_TAG_ATTESTATION_CHALLENGE, attestationChallenge);
+ // Return early if the caller did not request any device identifiers to be included in the
+ // attestation record.
+ if (idTypes == null) {
+ return attestArgs;
+ }
final Set<Integer> idTypesSet = new ArraySet<>(idTypes.length);
for (int idType : idTypes) {
idTypesSet.add(idType);
@@ -191,6 +178,44 @@
Build.MANUFACTURER.getBytes(StandardCharsets.UTF_8));
attestArgs.addBytes(KeymasterDefs.KM_TAG_ATTESTATION_ID_MODEL,
Build.MODEL.getBytes(StandardCharsets.UTF_8));
+ return attestArgs;
+ }
+
+ /**
+ * Performs attestation of the device's identifiers. This method returns a certificate chain
+ * whose first element contains the requested device identifiers in an extension. The device's
+ * manufacturer, model, brand, device and product are always also included in the attestation.
+ * If the device supports attestation in secure hardware, the chain will be rooted at a
+ * trustworthy CA key. Otherwise, the chain will be rooted at an untrusted certificate. See
+ * <a href="https://developer.android.com/training/articles/security-key-attestation.html">
+ * Key Attestation</a> for the format of the certificate extension.
+ * <p>
+ * Attestation will only be successful when all of the following are true:
+ * 1) The device has been set up to support device identifier attestation at the factory.
+ * 2) The user has not permanently disabled device identifier attestation.
+ * 3) You have permission to access the device identifiers you are requesting attestation for.
+ * <p>
+ * For privacy reasons, you cannot distinguish between (1) and (2). If attestation is
+ * unsuccessful, the device may not support it in general or the user may have permanently
+ * disabled it.
+ *
+ * @param context the context to use for retrieving device identifiers.
+ * @param idTypes the types of device identifiers to attest.
+ * @param attestationChallenge a blob to include in the certificate alongside the device
+ * identifiers.
+ *
+ * @return a certificate chain containing the requested device identifiers in the first element
+ *
+ * @exception SecurityException if you are not permitted to obtain an attestation of the
+ * device's identifiers.
+ * @exception DeviceIdAttestationException if the attestation operation fails.
+ */
+ @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ @NonNull public static X509Certificate[] attestDeviceIds(Context context,
+ @NonNull int[] idTypes, @NonNull byte[] attestationChallenge) throws
+ DeviceIdAttestationException {
+ final KeymasterArguments attestArgs = prepareAttestationArgumentsForDeviceId(
+ context, idTypes, attestationChallenge);
// Perform attestation.
final KeymasterCertificateChain outChain = new KeymasterCertificateChain();
diff --git a/android/security/keystore/BackwardsCompat.java b/android/security/keystore/BackwardsCompat.java
new file mode 100644
index 0000000..69558c4
--- /dev/null
+++ b/android/security/keystore/BackwardsCompat.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2018 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.security.keystore;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * Helpers for converting classes between old and new API, so we can preserve backwards
+ * compatibility while teamfooding. This will be removed soon.
+ *
+ * @hide
+ */
+class BackwardsCompat {
+
+
+ static KeychainProtectionParams toLegacyKeychainProtectionParams(
+ android.security.keystore.recovery.KeyChainProtectionParams keychainProtectionParams
+ ) {
+ return new KeychainProtectionParams.Builder()
+ .setUserSecretType(keychainProtectionParams.getUserSecretType())
+ .setSecret(keychainProtectionParams.getSecret())
+ .setLockScreenUiFormat(keychainProtectionParams.getLockScreenUiFormat())
+ .setKeyDerivationParams(
+ toLegacyKeyDerivationParams(
+ keychainProtectionParams.getKeyDerivationParams()))
+ .build();
+ }
+
+ static KeyDerivationParams toLegacyKeyDerivationParams(
+ android.security.keystore.recovery.KeyDerivationParams keyDerivationParams
+ ) {
+ return new KeyDerivationParams(
+ keyDerivationParams.getAlgorithm(), keyDerivationParams.getSalt());
+ }
+
+ static WrappedApplicationKey toLegacyWrappedApplicationKey(
+ android.security.keystore.recovery.WrappedApplicationKey wrappedApplicationKey
+ ) {
+ return new WrappedApplicationKey.Builder()
+ .setAlias(wrappedApplicationKey.getAlias())
+ .setEncryptedKeyMaterial(wrappedApplicationKey.getEncryptedKeyMaterial())
+ .build();
+ }
+
+ static android.security.keystore.recovery.KeyDerivationParams fromLegacyKeyDerivationParams(
+ KeyDerivationParams keyDerivationParams
+ ) {
+ return new android.security.keystore.recovery.KeyDerivationParams(
+ keyDerivationParams.getAlgorithm(), keyDerivationParams.getSalt());
+ }
+
+ static android.security.keystore.recovery.WrappedApplicationKey fromLegacyWrappedApplicationKey(
+ WrappedApplicationKey wrappedApplicationKey
+ ) {
+ return new android.security.keystore.recovery.WrappedApplicationKey.Builder()
+ .setAlias(wrappedApplicationKey.getAlias())
+ .setEncryptedKeyMaterial(wrappedApplicationKey.getEncryptedKeyMaterial())
+ .build();
+ }
+
+ static List<android.security.keystore.recovery.WrappedApplicationKey>
+ fromLegacyWrappedApplicationKeys(List<WrappedApplicationKey> wrappedApplicationKeys
+ ) {
+ return map(wrappedApplicationKeys, BackwardsCompat::fromLegacyWrappedApplicationKey);
+ }
+
+ static List<android.security.keystore.recovery.KeyChainProtectionParams>
+ fromLegacyKeychainProtectionParams(
+ List<KeychainProtectionParams> keychainProtectionParams) {
+ return map(keychainProtectionParams, BackwardsCompat::fromLegacyKeychainProtectionParam);
+ }
+
+ static android.security.keystore.recovery.KeyChainProtectionParams
+ fromLegacyKeychainProtectionParam(KeychainProtectionParams keychainProtectionParams) {
+ return new android.security.keystore.recovery.KeyChainProtectionParams.Builder()
+ .setUserSecretType(keychainProtectionParams.getUserSecretType())
+ .setSecret(keychainProtectionParams.getSecret())
+ .setLockScreenUiFormat(keychainProtectionParams.getLockScreenUiFormat())
+ .setKeyDerivationParams(
+ fromLegacyKeyDerivationParams(
+ keychainProtectionParams.getKeyDerivationParams()))
+ .build();
+ }
+
+ static KeychainSnapshot toLegacyKeychainSnapshot(
+ android.security.keystore.recovery.KeyChainSnapshot keychainSnapshot
+ ) {
+ return new KeychainSnapshot.Builder()
+ .setCounterId(keychainSnapshot.getCounterId())
+ .setEncryptedRecoveryKeyBlob(keychainSnapshot.getEncryptedRecoveryKeyBlob())
+ .setTrustedHardwarePublicKey(keychainSnapshot.getTrustedHardwarePublicKey())
+ .setSnapshotVersion(keychainSnapshot.getSnapshotVersion())
+ .setMaxAttempts(keychainSnapshot.getMaxAttempts())
+ .setServerParams(keychainSnapshot.getServerParams())
+ .setKeychainProtectionParams(
+ map(keychainSnapshot.getKeyChainProtectionParams(),
+ BackwardsCompat::toLegacyKeychainProtectionParams))
+ .setWrappedApplicationKeys(
+ map(keychainSnapshot.getWrappedApplicationKeys(),
+ BackwardsCompat::toLegacyWrappedApplicationKey))
+ .build();
+ }
+
+ static <A, B> List<B> map(List<A> as, Function<A, B> f) {
+ ArrayList<B> bs = new ArrayList<>(as.size());
+ for (A a : as) {
+ bs.add(f.apply(a));
+ }
+ return bs;
+ }
+}
diff --git a/android/os/Seccomp.java b/android/security/keystore/BadCertificateFormatException.java
similarity index 63%
copy from android/os/Seccomp.java
copy to android/security/keystore/BadCertificateFormatException.java
index f14e93f..ddc7bd2 100644
--- a/android/os/Seccomp.java
+++ b/android/security/keystore/BadCertificateFormatException.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright (C) 2018 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.
@@ -14,11 +14,15 @@
* limitations under the License.
*/
-package android.os;
+package android.security.keystore;
/**
+ * Error thrown when the recovery agent supplies an invalid X509 certificate.
+ *
* @hide
*/
-public final class Seccomp {
- public static final native void setPolicy();
+public class BadCertificateFormatException extends RecoveryControllerException {
+ public BadCertificateFormatException(String msg) {
+ super(msg);
+ }
}
diff --git a/android/security/keystore/DecryptionFailedException.java b/android/security/keystore/DecryptionFailedException.java
new file mode 100644
index 0000000..945fcf6
--- /dev/null
+++ b/android/security/keystore/DecryptionFailedException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2018 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.security.keystore;
+
+/**
+ * Error thrown when decryption failed, due to an agent error. i.e., using the incorrect key,
+ * trying to decrypt garbage data, trying to decrypt data that has somehow been corrupted, etc.
+ *
+ * @hide
+ */
+public class DecryptionFailedException extends RecoveryControllerException {
+
+ public DecryptionFailedException(String msg) {
+ super(msg);
+ }
+}
diff --git a/android/security/keystore/InternalRecoveryServiceException.java b/android/security/keystore/InternalRecoveryServiceException.java
new file mode 100644
index 0000000..85829be
--- /dev/null
+++ b/android/security/keystore/InternalRecoveryServiceException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 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.security.keystore;
+
+/**
+ * An error thrown when something went wrong internally in the recovery service.
+ *
+ * <p>This is an unexpected error, and indicates a problem with the service itself, rather than the
+ * caller having performed some kind of illegal action.
+ *
+ * @hide
+ */
+public class InternalRecoveryServiceException extends RecoveryControllerException {
+ public InternalRecoveryServiceException(String msg) {
+ super(msg);
+ }
+
+ public InternalRecoveryServiceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/android/security/recoverablekeystore/KeyDerivationParameters.java b/android/security/keystore/KeyDerivationParams.java
similarity index 68%
rename from android/security/recoverablekeystore/KeyDerivationParameters.java
rename to android/security/keystore/KeyDerivationParams.java
index 978e60e..b19cee2 100644
--- a/android/security/recoverablekeystore/KeyDerivationParameters.java
+++ b/android/security/keystore/KeyDerivationParams.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package android.security.recoverablekeystore;
+package android.security.keystore;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -28,21 +28,17 @@
/**
* Collection of parameters which define a key derivation function.
- * Supports
+ * Currently only supports salted SHA-256
*
- * <ul>
- * <li>SHA256
- * <li>Argon2id
- * </ul>
* @hide
*/
-public final class KeyDerivationParameters implements Parcelable {
+public final class KeyDerivationParams implements Parcelable {
private final int mAlgorithm;
private byte[] mSalt;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({ALGORITHM_SHA256, ALGORITHM_ARGON2ID})
+ @IntDef(prefix = {"ALGORITHM_"}, value = {ALGORITHM_SHA256, ALGORITHM_ARGON2ID})
public @interface KeyDerivationAlgorithm {
}
@@ -53,6 +49,7 @@
/**
* Argon2ID
+ * @hide
*/
// TODO: add Argon2ID support.
public static final int ALGORITHM_ARGON2ID = 2;
@@ -60,11 +57,11 @@
/**
* Creates instance of the class to to derive key using salted SHA256 hash.
*/
- public static KeyDerivationParameters createSHA256Parameters(@NonNull byte[] salt) {
- return new KeyDerivationParameters(ALGORITHM_SHA256, salt);
+ public static KeyDerivationParams createSha256Params(@NonNull byte[] salt) {
+ return new KeyDerivationParams(ALGORITHM_SHA256, salt);
}
- private KeyDerivationParameters(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) {
+ KeyDerivationParams(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) {
mAlgorithm = algorithm;
mSalt = Preconditions.checkNotNull(salt);
}
@@ -83,24 +80,30 @@
return mSalt;
}
- public static final Parcelable.Creator<KeyDerivationParameters> CREATOR =
- new Parcelable.Creator<KeyDerivationParameters>() {
- public KeyDerivationParameters createFromParcel(Parcel in) {
- return new KeyDerivationParameters(in);
+ public static final Parcelable.Creator<KeyDerivationParams> CREATOR =
+ new Parcelable.Creator<KeyDerivationParams>() {
+ public KeyDerivationParams createFromParcel(Parcel in) {
+ return new KeyDerivationParams(in);
}
- public KeyDerivationParameters[] newArray(int length) {
- return new KeyDerivationParameters[length];
+ public KeyDerivationParams[] newArray(int length) {
+ return new KeyDerivationParams[length];
}
};
+ /**
+ * @hide
+ */
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(mAlgorithm);
out.writeByteArray(mSalt);
}
- protected KeyDerivationParameters(Parcel in) {
+ /**
+ * @hide
+ */
+ protected KeyDerivationParams(Parcel in) {
mAlgorithm = in.readInt();
mSalt = in.createByteArray();
}
diff --git a/android/security/keystore/KeyGenParameterSpec.java b/android/security/keystore/KeyGenParameterSpec.java
index 1238d87..1e2b873 100644
--- a/android/security/keystore/KeyGenParameterSpec.java
+++ b/android/security/keystore/KeyGenParameterSpec.java
@@ -262,6 +262,7 @@
private final boolean mUniqueIdIncluded;
private final boolean mUserAuthenticationValidWhileOnBody;
private final boolean mInvalidatedByBiometricEnrollment;
+ private final boolean mIsStrongBoxBacked;
/**
* @hide should be built with Builder
@@ -289,7 +290,8 @@
byte[] attestationChallenge,
boolean uniqueIdIncluded,
boolean userAuthenticationValidWhileOnBody,
- boolean invalidatedByBiometricEnrollment) {
+ boolean invalidatedByBiometricEnrollment,
+ boolean isStrongBoxBacked) {
if (TextUtils.isEmpty(keyStoreAlias)) {
throw new IllegalArgumentException("keyStoreAlias must not be empty");
}
@@ -335,6 +337,7 @@
mUniqueIdIncluded = uniqueIdIncluded;
mUserAuthenticationValidWhileOnBody = userAuthenticationValidWhileOnBody;
mInvalidatedByBiometricEnrollment = invalidatedByBiometricEnrollment;
+ mIsStrongBoxBacked = isStrongBoxBacked;
}
/**
@@ -625,6 +628,13 @@
}
/**
+ * Returns {@code true} if the key is protected by a Strongbox security chip.
+ */
+ public boolean isStrongBoxBacked() {
+ return mIsStrongBoxBacked;
+ }
+
+ /**
* Builder of {@link KeyGenParameterSpec} instances.
*/
public final static class Builder {
@@ -652,6 +662,7 @@
private boolean mUniqueIdIncluded = false;
private boolean mUserAuthenticationValidWhileOnBody;
private boolean mInvalidatedByBiometricEnrollment = true;
+ private boolean mIsStrongBoxBacked = false;
/**
* Creates a new instance of the {@code Builder}.
@@ -1177,6 +1188,15 @@
}
/**
+ * Sets whether this key should be protected by a StrongBox security chip.
+ */
+ @NonNull
+ public Builder setIsStrongBoxBacked(boolean isStrongBoxBacked) {
+ mIsStrongBoxBacked = isStrongBoxBacked;
+ return this;
+ }
+
+ /**
* Builds an instance of {@code KeyGenParameterSpec}.
*/
@NonNull
@@ -1204,7 +1224,8 @@
mAttestationChallenge,
mUniqueIdIncluded,
mUserAuthenticationValidWhileOnBody,
- mInvalidatedByBiometricEnrollment);
+ mInvalidatedByBiometricEnrollment,
+ mIsStrongBoxBacked);
}
}
}
diff --git a/android/security/keystore/KeyProperties.java b/android/security/keystore/KeyProperties.java
index a250d1f..f54b6de 100644
--- a/android/security/keystore/KeyProperties.java
+++ b/android/security/keystore/KeyProperties.java
@@ -44,6 +44,7 @@
PURPOSE_DECRYPT,
PURPOSE_SIGN,
PURPOSE_VERIFY,
+ PURPOSE_WRAP_KEY,
})
public @interface PurposeEnum {}
@@ -68,6 +69,11 @@
public static final int PURPOSE_VERIFY = 1 << 3;
/**
+ * Purpose of key: wrapping and unwrapping wrapped keys for secure import.
+ */
+ public static final int PURPOSE_WRAP_KEY = 1 << 5;
+
+ /**
* @hide
*/
public static abstract class Purpose {
@@ -83,6 +89,8 @@
return KeymasterDefs.KM_PURPOSE_SIGN;
case PURPOSE_VERIFY:
return KeymasterDefs.KM_PURPOSE_VERIFY;
+ case PURPOSE_WRAP_KEY:
+ return KeymasterDefs.KM_PURPOSE_WRAP;
default:
throw new IllegalArgumentException("Unknown purpose: " + purpose);
}
@@ -98,6 +106,8 @@
return PURPOSE_SIGN;
case KeymasterDefs.KM_PURPOSE_VERIFY:
return PURPOSE_VERIFY;
+ case KeymasterDefs.KM_PURPOSE_WRAP:
+ return PURPOSE_WRAP_KEY;
default:
throw new IllegalArgumentException("Unknown purpose: " + purpose);
}
@@ -146,6 +156,15 @@
/** Advanced Encryption Standard (AES) key. */
public static final String KEY_ALGORITHM_AES = "AES";
+ /**
+ * Triple Data Encryption Algorithm (3DES) key.
+ *
+ * @deprecated Included for interoperability with legacy systems. Prefer {@link
+ * KeyProperties#KEY_ALGORITHM_AES} for new development.
+ */
+ @Deprecated
+ public static final String KEY_ALGORITHM_3DES = "DESede";
+
/** Keyed-Hash Message Authentication Code (HMAC) key using SHA-1 as the hash. */
public static final String KEY_ALGORITHM_HMAC_SHA1 = "HmacSHA1";
@@ -196,6 +215,8 @@
@NonNull @KeyAlgorithmEnum String algorithm) {
if (KEY_ALGORITHM_AES.equalsIgnoreCase(algorithm)) {
return KeymasterDefs.KM_ALGORITHM_AES;
+ } else if (KEY_ALGORITHM_3DES.equalsIgnoreCase(algorithm)) {
+ return KeymasterDefs.KM_ALGORITHM_3DES;
} else if (algorithm.toUpperCase(Locale.US).startsWith("HMAC")) {
return KeymasterDefs.KM_ALGORITHM_HMAC;
} else {
@@ -210,6 +231,8 @@
switch (keymasterAlgorithm) {
case KeymasterDefs.KM_ALGORITHM_AES:
return KEY_ALGORITHM_AES;
+ case KeymasterDefs.KM_ALGORITHM_3DES:
+ return KEY_ALGORITHM_3DES;
case KeymasterDefs.KM_ALGORITHM_HMAC:
switch (keymasterDigest) {
case KeymasterDefs.KM_DIGEST_SHA1:
@@ -666,6 +689,10 @@
*/
public static final int ORIGIN_UNKNOWN = 1 << 2;
+ /** Key was imported into the AndroidKeyStore in an encrypted wrapper */
+ public static final int ORIGIN_SECURELY_IMPORTED = 1 << 3;
+
+
/**
* @hide
*/
@@ -680,6 +707,8 @@
return ORIGIN_IMPORTED;
case KeymasterDefs.KM_ORIGIN_UNKNOWN:
return ORIGIN_UNKNOWN;
+ case KeymasterDefs.KM_ORIGIN_SECURELY_IMPORTED:
+ return ORIGIN_SECURELY_IMPORTED;
default:
throw new IllegalArgumentException("Unknown origin: " + origin);
}
diff --git a/android/security/keystore/KeyProtection.java b/android/security/keystore/KeyProtection.java
index 2eb0663..dbacb9c 100644
--- a/android/security/keystore/KeyProtection.java
+++ b/android/security/keystore/KeyProtection.java
@@ -488,9 +488,9 @@
private int mUserAuthenticationValidityDurationSeconds = -1;
private boolean mUserAuthenticationValidWhileOnBody;
private boolean mInvalidatedByBiometricEnrollment = true;
-
private long mBoundToSecureUserId = GateKeeper.INVALID_SECURE_USER_ID;
private boolean mCriticalToDeviceEncryption = false;
+
/**
* Creates a new instance of the {@code Builder}.
*
diff --git a/android/security/keystore/KeychainProtectionParams.java b/android/security/keystore/KeychainProtectionParams.java
new file mode 100644
index 0000000..a940fdc
--- /dev/null
+++ b/android/security/keystore/KeychainProtectionParams.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2017 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.security.keystore;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+
+/**
+ * A {@link KeychainSnapshot} is protected with a key derived from the user's lock screen. This
+ * class wraps all the data necessary to derive the same key on a recovering device:
+ *
+ * <ul>
+ * <li>UI parameters for the user's lock screen - so that if e.g., the user was using a pattern,
+ * the recovering device can display the pattern UI to the user when asking them to enter
+ * the lock screen from their previous device.
+ * <li>The algorithm used to derive a key from the user's lock screen, e.g. SHA-256 with a salt.
+ * </ul>
+ *
+ * <p>As such, this data is sent along with the {@link KeychainSnapshot} when syncing the current
+ * version of the keychain.
+ *
+ * <p>For now, the recoverable keychain only supports a single layer of protection, which is the
+ * user's lock screen. In the future, the keychain will support multiple layers of protection
+ * (e.g. an additional keychain password, along with the lock screen).
+ *
+ * @hide
+ */
+public final class KeychainProtectionParams implements Parcelable {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_LOCKSCREEN, TYPE_CUSTOM_PASSWORD})
+ public @interface UserSecretType {
+ }
+
+ /**
+ * Lockscreen secret is required to recover KeyStore.
+ */
+ public static final int TYPE_LOCKSCREEN = 100;
+
+ /**
+ * Custom passphrase, unrelated to lock screen, is required to recover KeyStore.
+ */
+ public static final int TYPE_CUSTOM_PASSWORD = 101;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_PIN, TYPE_PASSWORD, TYPE_PATTERN})
+ public @interface LockScreenUiFormat {
+ }
+
+ /**
+ * Pin with digits only.
+ */
+ public static final int TYPE_PIN = 1;
+
+ /**
+ * Password. String with latin-1 characters only.
+ */
+ public static final int TYPE_PASSWORD = 2;
+
+ /**
+ * Pattern with 3 by 3 grid.
+ */
+ public static final int TYPE_PATTERN = 3;
+
+ @UserSecretType
+ private Integer mUserSecretType;
+
+ @LockScreenUiFormat
+ private Integer mLockScreenUiFormat;
+
+ /**
+ * Parameters of the key derivation function, including algorithm, difficulty, salt.
+ */
+ private KeyDerivationParams mKeyDerivationParams;
+ private byte[] mSecret; // Derived from user secret. The field must have limited visibility.
+
+ /**
+ * @param secret Constructor creates a reference to the secret. Caller must use
+ * @link {#clearSecret} to overwrite its value in memory.
+ * @hide
+ */
+ public KeychainProtectionParams(@UserSecretType int userSecretType,
+ @LockScreenUiFormat int lockScreenUiFormat,
+ @NonNull KeyDerivationParams keyDerivationParams,
+ @NonNull byte[] secret) {
+ mUserSecretType = userSecretType;
+ mLockScreenUiFormat = lockScreenUiFormat;
+ mKeyDerivationParams = Preconditions.checkNotNull(keyDerivationParams);
+ mSecret = Preconditions.checkNotNull(secret);
+ }
+
+ private KeychainProtectionParams() {
+
+ }
+
+ /**
+ * @see TYPE_LOCKSCREEN
+ * @see TYPE_CUSTOM_PASSWORD
+ */
+ public @UserSecretType int getUserSecretType() {
+ return mUserSecretType;
+ }
+
+ /**
+ * Specifies UX shown to user during recovery.
+ * Default value is {@code TYPE_LOCKSCREEN}
+ *
+ * @see TYPE_PIN
+ * @see TYPE_PASSWORD
+ * @see TYPE_PATTERN
+ */
+ public @LockScreenUiFormat int getLockScreenUiFormat() {
+ return mLockScreenUiFormat;
+ }
+
+ /**
+ * Specifies function used to derive symmetric key from user input
+ * Format is defined in separate util class.
+ */
+ public @NonNull KeyDerivationParams getKeyDerivationParams() {
+ return mKeyDerivationParams;
+ }
+
+ /**
+ * Secret derived from user input.
+ * Default value is empty array
+ *
+ * @return secret or empty array
+ */
+ public @NonNull byte[] getSecret() {
+ return mSecret;
+ }
+
+ /**
+ * Builder for creating {@link KeychainProtectionParams}.
+ */
+ public static class Builder {
+ private KeychainProtectionParams mInstance = new KeychainProtectionParams();
+
+ /**
+ * Sets user secret type.
+ *
+ * @see TYPE_LOCKSCREEN
+ * @see TYPE_CUSTOM_PASSWORD
+ * @param userSecretType The secret type
+ * @return This builder.
+ */
+ public Builder setUserSecretType(@UserSecretType int userSecretType) {
+ mInstance.mUserSecretType = userSecretType;
+ return this;
+ }
+
+ /**
+ * Sets UI format.
+ *
+ * @see TYPE_PIN
+ * @see TYPE_PASSWORD
+ * @see TYPE_PATTERN
+ * @param lockScreenUiFormat The UI format
+ * @return This builder.
+ */
+ public Builder setLockScreenUiFormat(@LockScreenUiFormat int lockScreenUiFormat) {
+ mInstance.mLockScreenUiFormat = lockScreenUiFormat;
+ return this;
+ }
+
+ /**
+ * Sets parameters of the key derivation function.
+ *
+ * @param keyDerivationParams Key derivation Params
+ * @return This builder.
+ */
+ public Builder setKeyDerivationParams(@NonNull KeyDerivationParams
+ keyDerivationParams) {
+ mInstance.mKeyDerivationParams = keyDerivationParams;
+ return this;
+ }
+
+ /**
+ * Secret derived from user input, or empty array.
+ *
+ * @param secret The secret.
+ * @return This builder.
+ */
+ public Builder setSecret(@NonNull byte[] secret) {
+ mInstance.mSecret = secret;
+ return this;
+ }
+
+
+ /**
+ * Creates a new {@link KeychainProtectionParams} instance.
+ * The instance will include default values, if {@link setSecret}
+ * or {@link setUserSecretType} were not called.
+ *
+ * @return new instance
+ * @throws NullPointerException if some required fields were not set.
+ */
+ @NonNull public KeychainProtectionParams build() {
+ if (mInstance.mUserSecretType == null) {
+ mInstance.mUserSecretType = TYPE_LOCKSCREEN;
+ }
+ Preconditions.checkNotNull(mInstance.mLockScreenUiFormat);
+ Preconditions.checkNotNull(mInstance.mKeyDerivationParams);
+ if (mInstance.mSecret == null) {
+ mInstance.mSecret = new byte[]{};
+ }
+ return mInstance;
+ }
+ }
+
+ /**
+ * Removes secret from memory than object is no longer used.
+ * Since finalizer call is not reliable, please use @link {#clearSecret} directly.
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ clearSecret();
+ super.finalize();
+ }
+
+ /**
+ * Fills mSecret with zeroes.
+ */
+ public void clearSecret() {
+ Arrays.fill(mSecret, (byte) 0);
+ }
+
+ public static final Parcelable.Creator<KeychainProtectionParams> CREATOR =
+ new Parcelable.Creator<KeychainProtectionParams>() {
+ public KeychainProtectionParams createFromParcel(Parcel in) {
+ return new KeychainProtectionParams(in);
+ }
+
+ public KeychainProtectionParams[] newArray(int length) {
+ return new KeychainProtectionParams[length];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mUserSecretType);
+ out.writeInt(mLockScreenUiFormat);
+ out.writeTypedObject(mKeyDerivationParams, flags);
+ out.writeByteArray(mSecret);
+ }
+
+ /**
+ * @hide
+ */
+ protected KeychainProtectionParams(Parcel in) {
+ mUserSecretType = in.readInt();
+ mLockScreenUiFormat = in.readInt();
+ mKeyDerivationParams = in.readTypedObject(KeyDerivationParams.CREATOR);
+ mSecret = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/security/keystore/KeychainSnapshot.java b/android/security/keystore/KeychainSnapshot.java
new file mode 100644
index 0000000..23aec25
--- /dev/null
+++ b/android/security/keystore/KeychainSnapshot.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2017 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.security.keystore;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+
+/**
+ * A snapshot of a version of the keystore. Two events can trigger the generation of a new snapshot:
+ *
+ * <ul>
+ * <li>The user's lock screen changes. (A key derived from the user's lock screen is used to
+ * protected the keychain, which is why this forces a new snapshot.)
+ * <li>A key is added to or removed from the recoverable keychain.
+ * </ul>
+ *
+ * <p>The snapshot data is also encrypted with the remote trusted hardware's public key, so even
+ * the recovery agent itself should not be able to decipher the data. The recovery agent sends an
+ * instance of this to the remote trusted hardware whenever a new snapshot is generated. During a
+ * recovery flow, the recovery agent retrieves a snapshot from the remote trusted hardware. It then
+ * sends it to the framework, where it is decrypted using the user's lock screen from their previous
+ * device.
+ *
+ * @hide
+ */
+public final class KeychainSnapshot implements Parcelable {
+ private static final int DEFAULT_MAX_ATTEMPTS = 10;
+ private static final long DEFAULT_COUNTER_ID = 1L;
+
+ private int mSnapshotVersion;
+ private int mMaxAttempts = DEFAULT_MAX_ATTEMPTS;
+ private long mCounterId = DEFAULT_COUNTER_ID;
+ private byte[] mServerParams;
+ private byte[] mPublicKey;
+ private List<KeychainProtectionParams> mKeychainProtectionParams;
+ private List<WrappedApplicationKey> mEntryRecoveryData;
+ private byte[] mEncryptedRecoveryKeyBlob;
+
+ /**
+ * @hide
+ * Deprecated, consider using builder.
+ */
+ public KeychainSnapshot(
+ int snapshotVersion,
+ @NonNull List<KeychainProtectionParams> keychainProtectionParams,
+ @NonNull List<WrappedApplicationKey> wrappedApplicationKeys,
+ @NonNull byte[] encryptedRecoveryKeyBlob) {
+ mSnapshotVersion = snapshotVersion;
+ mKeychainProtectionParams =
+ Preconditions.checkCollectionElementsNotNull(keychainProtectionParams,
+ "keychainProtectionParams");
+ mEntryRecoveryData = Preconditions.checkCollectionElementsNotNull(wrappedApplicationKeys,
+ "wrappedApplicationKeys");
+ mEncryptedRecoveryKeyBlob = Preconditions.checkNotNull(encryptedRecoveryKeyBlob);
+ }
+
+ private KeychainSnapshot() {
+
+ }
+
+ /**
+ * Snapshot version for given account. It is incremented when user secret or list of application
+ * keys changes.
+ */
+ public int getSnapshotVersion() {
+ return mSnapshotVersion;
+ }
+
+ /**
+ * Number of user secret guesses allowed during Keychain recovery.
+ */
+ public int getMaxAttempts() {
+ return mMaxAttempts;
+ }
+
+ /**
+ * CounterId which is rotated together with user secret.
+ */
+ public long getCounterId() {
+ return mCounterId;
+ }
+
+ /**
+ * Server parameters.
+ */
+ public @NonNull byte[] getServerParams() {
+ return mServerParams;
+ }
+
+ /**
+ * Public key used to encrypt {@code encryptedRecoveryKeyBlob}.
+ *
+ * See implementation for binary key format
+ */
+ // TODO: document key format.
+ public @NonNull byte[] getTrustedHardwarePublicKey() {
+ return mPublicKey;
+ }
+
+ /**
+ * UI and key derivation parameters. Note that combination of secrets may be used.
+ */
+ public @NonNull List<KeychainProtectionParams> getKeychainProtectionParams() {
+ return mKeychainProtectionParams;
+ }
+
+ /**
+ * List of application keys, with key material encrypted by
+ * the recovery key ({@link #getEncryptedRecoveryKeyBlob}).
+ */
+ public @NonNull List<WrappedApplicationKey> getWrappedApplicationKeys() {
+ return mEntryRecoveryData;
+ }
+
+ /**
+ * Recovery key blob, encrypted by user secret and recovery service public key.
+ */
+ public @NonNull byte[] getEncryptedRecoveryKeyBlob() {
+ return mEncryptedRecoveryKeyBlob;
+ }
+
+ public static final Parcelable.Creator<KeychainSnapshot> CREATOR =
+ new Parcelable.Creator<KeychainSnapshot>() {
+ public KeychainSnapshot createFromParcel(Parcel in) {
+ return new KeychainSnapshot(in);
+ }
+
+ public KeychainSnapshot[] newArray(int length) {
+ return new KeychainSnapshot[length];
+ }
+ };
+
+ /**
+ * Builder for creating {@link KeychainSnapshot}.
+ *
+ * @hide
+ */
+ public static class Builder {
+ private KeychainSnapshot mInstance = new KeychainSnapshot();
+
+ /**
+ * Snapshot version for given account.
+ *
+ * @param snapshotVersion The snapshot version
+ * @return This builder.
+ */
+ public Builder setSnapshotVersion(int snapshotVersion) {
+ mInstance.mSnapshotVersion = snapshotVersion;
+ return this;
+ }
+
+ /**
+ * Sets the number of user secret guesses allowed during Keychain recovery.
+ *
+ * @param maxAttempts The maximum number of guesses.
+ * @return This builder.
+ */
+ public Builder setMaxAttempts(int maxAttempts) {
+ mInstance.mMaxAttempts = maxAttempts;
+ return this;
+ }
+
+ /**
+ * Sets counter id.
+ *
+ * @param counterId The counter id.
+ * @return This builder.
+ */
+ public Builder setCounterId(long counterId) {
+ mInstance.mCounterId = counterId;
+ return this;
+ }
+
+ /**
+ * Sets server parameters.
+ *
+ * @param serverParams The server parameters
+ * @return This builder.
+ */
+ public Builder setServerParams(byte[] serverParams) {
+ mInstance.mServerParams = serverParams;
+ return this;
+ }
+
+ /**
+ * Sets public key used to encrypt recovery blob.
+ *
+ * @param publicKey The public key
+ * @return This builder.
+ */
+ public Builder setTrustedHardwarePublicKey(byte[] publicKey) {
+ mInstance.mPublicKey = publicKey;
+ return this;
+ }
+
+ /**
+ * Sets UI and key derivation parameters
+ *
+ * @param recoveryMetadata The UI and key derivation parameters
+ * @return This builder.
+ */
+ public Builder setKeychainProtectionParams(
+ @NonNull List<KeychainProtectionParams> recoveryMetadata) {
+ mInstance.mKeychainProtectionParams = recoveryMetadata;
+ return this;
+ }
+
+ /**
+ * List of application keys.
+ *
+ * @param entryRecoveryData List of application keys
+ * @return This builder.
+ */
+ public Builder setWrappedApplicationKeys(List<WrappedApplicationKey> entryRecoveryData) {
+ mInstance.mEntryRecoveryData = entryRecoveryData;
+ return this;
+ }
+
+ /**
+ * Sets recovery key blob
+ *
+ * @param encryptedRecoveryKeyBlob The recovery key blob.
+ * @return This builder.
+ */
+ public Builder setEncryptedRecoveryKeyBlob(@NonNull byte[] encryptedRecoveryKeyBlob) {
+ mInstance.mEncryptedRecoveryKeyBlob = encryptedRecoveryKeyBlob;
+ return this;
+ }
+
+
+ /**
+ * Creates a new {@link KeychainSnapshot} instance.
+ *
+ * @return new instance
+ * @throws NullPointerException if some required fields were not set.
+ */
+ @NonNull public KeychainSnapshot build() {
+ Preconditions.checkCollectionElementsNotNull(mInstance.mKeychainProtectionParams,
+ "recoveryMetadata");
+ Preconditions.checkCollectionElementsNotNull(mInstance.mEntryRecoveryData,
+ "entryRecoveryData");
+ Preconditions.checkNotNull(mInstance.mEncryptedRecoveryKeyBlob);
+ Preconditions.checkNotNull(mInstance.mServerParams);
+ Preconditions.checkNotNull(mInstance.mPublicKey);
+ return mInstance;
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mSnapshotVersion);
+ out.writeTypedList(mKeychainProtectionParams);
+ out.writeByteArray(mEncryptedRecoveryKeyBlob);
+ out.writeTypedList(mEntryRecoveryData);
+ }
+
+ /**
+ * @hide
+ */
+ protected KeychainSnapshot(Parcel in) {
+ mSnapshotVersion = in.readInt();
+ mKeychainProtectionParams = in.createTypedArrayList(KeychainProtectionParams.CREATOR);
+ mEncryptedRecoveryKeyBlob = in.createByteArray();
+ mEntryRecoveryData = in.createTypedArrayList(WrappedApplicationKey.CREATOR);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/security/keystore/LockScreenRequiredException.java b/android/security/keystore/LockScreenRequiredException.java
new file mode 100644
index 0000000..b07fb9c
--- /dev/null
+++ b/android/security/keystore/LockScreenRequiredException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2018 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.security.keystore;
+
+/**
+ * Error thrown when trying to generate keys for a profile that has no lock screen set.
+ *
+ * <p>A lock screen must be set, as the lock screen is used to encrypt the snapshot.
+ *
+ * @hide
+ */
+public class LockScreenRequiredException extends RecoveryControllerException {
+ public LockScreenRequiredException(String msg) {
+ super(msg);
+ }
+}
diff --git a/android/security/keystore/RecoveryClaim.java b/android/security/keystore/RecoveryClaim.java
new file mode 100644
index 0000000..6f566af
--- /dev/null
+++ b/android/security/keystore/RecoveryClaim.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2018 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.security.keystore;
+
+/**
+ * An attempt to recover a keychain protected by remote secure hardware.
+ *
+ * @hide
+ */
+public class RecoveryClaim {
+
+ private final RecoverySession mRecoverySession;
+ private final byte[] mClaimBytes;
+
+ RecoveryClaim(RecoverySession recoverySession, byte[] claimBytes) {
+ mRecoverySession = recoverySession;
+ mClaimBytes = claimBytes;
+ }
+
+ /**
+ * Returns the session associated with the recovery attempt. This is used to match the symmetric
+ * key, which remains internal to the framework, for decrypting the claim response.
+ *
+ * @return The session data.
+ */
+ public RecoverySession getRecoverySession() {
+ return mRecoverySession;
+ }
+
+ /**
+ * Returns the encrypted claim's bytes.
+ *
+ * <p>This should be sent by the recovery agent to the remote secure hardware, which will use
+ * it to decrypt the keychain, before sending it re-encrypted with the session's symmetric key
+ * to the device.
+ */
+ public byte[] getClaimBytes() {
+ return mClaimBytes;
+ }
+}
diff --git a/android/security/keystore/RecoveryController.java b/android/security/keystore/RecoveryController.java
new file mode 100644
index 0000000..8be6d52
--- /dev/null
+++ b/android/security/keystore/RecoveryController.java
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2017 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.security.keystore;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import com.android.internal.widget.ILockSettings;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An assistant for generating {@link javax.crypto.SecretKey} instances that can be recovered by
+ * other Android devices belonging to the user. The exported keychain is protected by the user's
+ * lock screen.
+ *
+ * <p>The RecoveryController must be paired with a recovery agent. The recovery agent is responsible
+ * for transporting the keychain to remote trusted hardware. This hardware must prevent brute force
+ * attempts against the user's lock screen by limiting the number of allowed guesses (to, e.g., 10).
+ * After that number of incorrect guesses, the trusted hardware no longer allows access to the
+ * key chain.
+ *
+ * <p>For now only the recovery agent itself is able to create keys, so it is expected that the
+ * recovery agent is itself the system app.
+ *
+ * <p>A recovery agent requires the privileged permission
+ * {@code android.Manifest.permission#RECOVER_KEYSTORE}.
+ *
+ * @hide
+ */
+public class RecoveryController {
+ private static final String TAG = "RecoveryController";
+
+ /** Key has been successfully synced. */
+ public static final int RECOVERY_STATUS_SYNCED = 0;
+ /** Waiting for recovery agent to sync the key. */
+ public static final int RECOVERY_STATUS_SYNC_IN_PROGRESS = 1;
+ /** Recovery account is not available. */
+ public static final int RECOVERY_STATUS_MISSING_ACCOUNT = 2;
+ /** Key cannot be synced. */
+ public static final int RECOVERY_STATUS_PERMANENT_FAILURE = 3;
+
+ /**
+ * Failed because no snapshot is yet pending to be synced for the user.
+ *
+ * @hide
+ */
+ public static final int ERROR_NO_SNAPSHOT_PENDING = 21;
+
+ /**
+ * Failed due to an error internal to the recovery service. This is unexpected and indicates
+ * either a problem with the logic in the service, or a problem with a dependency of the
+ * service (such as AndroidKeyStore).
+ *
+ * @hide
+ */
+ public static final int ERROR_SERVICE_INTERNAL_ERROR = 22;
+
+ /**
+ * Failed because the user does not have a lock screen set.
+ *
+ * @hide
+ */
+ public static final int ERROR_INSECURE_USER = 23;
+
+ /**
+ * Error thrown when attempting to use a recovery session that has since been closed.
+ *
+ * @hide
+ */
+ public static final int ERROR_SESSION_EXPIRED = 24;
+
+ /**
+ * Failed because the provided certificate was not a valid X509 certificate.
+ *
+ * @hide
+ */
+ public static final int ERROR_BAD_CERTIFICATE_FORMAT = 25;
+
+ /**
+ * Error thrown if decryption failed. This might be because the tag is wrong, the key is wrong,
+ * the data has become corrupted, the data has been tampered with, etc.
+ *
+ * @hide
+ */
+ public static final int ERROR_DECRYPTION_FAILED = 26;
+
+
+ private final ILockSettings mBinder;
+
+ private RecoveryController(ILockSettings binder) {
+ mBinder = binder;
+ }
+
+ /**
+ * Gets a new instance of the class.
+ */
+ public static RecoveryController getInstance() {
+ ILockSettings lockSettings =
+ ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings"));
+ return new RecoveryController(lockSettings);
+ }
+
+ /**
+ * Initializes key recovery service for the calling application. RecoveryController
+ * randomly chooses one of the keys from the list and keeps it to use for future key export
+ * operations. Collection of all keys in the list must be signed by the provided {@code
+ * rootCertificateAlias}, which must also be present in the list of root certificates
+ * preinstalled on the device. The random selection allows RecoveryController to select
+ * which of a set of remote recovery service devices will be used.
+ *
+ * <p>In addition, RecoveryController enforces a delay of three months between
+ * consecutive initialization attempts, to limit the ability of an attacker to often switch
+ * remote recovery devices and significantly increase number of recovery attempts.
+ *
+ * @param rootCertificateAlias alias of a root certificate preinstalled on the device
+ * @param signedPublicKeyList binary blob a list of X509 certificates and signature
+ * @throws BadCertificateFormatException if the {@code signedPublicKeyList} is in a bad format.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void initRecoveryService(
+ @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList)
+ throws BadCertificateFormatException, InternalRecoveryServiceException {
+ try {
+ mBinder.initRecoveryService(rootCertificateAlias, signedPublicKeyList);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT) {
+ throw new BadCertificateFormatException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns data necessary to store all recoverable keys for given account. Key material is
+ * encrypted with user secret and recovery public key.
+ *
+ * @param account specific to Recovery agent.
+ * @return Data necessary to recover keystore.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public @NonNull KeychainSnapshot getRecoveryData(@NonNull byte[] account)
+ throws InternalRecoveryServiceException {
+ try {
+ return BackwardsCompat.toLegacyKeychainSnapshot(mBinder.getRecoveryData(account));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_NO_SNAPSHOT_PENDING) {
+ return null;
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Sets a listener which notifies recovery agent that new recovery snapshot is available. {@link
+ * #getRecoveryData} can be used to get the snapshot. Note that every recovery agent can have at
+ * most one registered listener at any time.
+ *
+ * @param intent triggered when new snapshot is available. Unregisters listener if the value is
+ * {@code null}.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent)
+ throws InternalRecoveryServiceException {
+ try {
+ mBinder.setSnapshotCreatedPendingIntent(intent);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns a map from recovery agent accounts to corresponding KeyStore recovery snapshot
+ * version. Version zero is used, if no snapshots were created for the account.
+ *
+ * @return Map from recovery agent accounts to snapshot versions.
+ * @see KeychainSnapshot#getSnapshotVersion
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public @NonNull Map<byte[], Integer> getRecoverySnapshotVersions()
+ throws InternalRecoveryServiceException {
+ try {
+ // IPC doesn't support generic Maps.
+ @SuppressWarnings("unchecked")
+ Map<byte[], Integer> result =
+ (Map<byte[], Integer>) mBinder.getRecoverySnapshotVersions();
+ return result;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Server parameters used to generate new recovery key blobs. This value will be included in
+ * {@code KeychainSnapshot.getEncryptedRecoveryKeyBlob()}. The same value must be included
+ * in vaultParams {@link #startRecoverySession}
+ *
+ * @param serverParams included in recovery key blob.
+ * @see #getRecoveryData
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void setServerParams(byte[] serverParams) throws InternalRecoveryServiceException {
+ try {
+ mBinder.setServerParams(serverParams);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Updates recovery status for given keys. It is used to notify keystore that key was
+ * successfully stored on the server or there were an error. Application can check this value
+ * using {@code getRecoveyStatus}.
+ *
+ * @param packageName Application whose recoverable keys' statuses are to be updated.
+ * @param aliases List of application-specific key aliases. If the array is empty, updates the
+ * status for all existing recoverable keys.
+ * @param status Status specific to recovery agent.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void setRecoveryStatus(
+ @NonNull String packageName, @Nullable String[] aliases, int status)
+ throws NameNotFoundException, InternalRecoveryServiceException {
+ try {
+ mBinder.setRecoveryStatus(packageName, aliases, status);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns a {@code Map} from Application's KeyStore key aliases to their recovery status.
+ * Negative status values are reserved for recovery agent specific codes. List of common codes:
+ *
+ * <ul>
+ * <li>{@link #RECOVERY_STATUS_SYNCED}
+ * <li>{@link #RECOVERY_STATUS_SYNC_IN_PROGRESS}
+ * <li>{@link #RECOVERY_STATUS_MISSING_ACCOUNT}
+ * <li>{@link #RECOVERY_STATUS_PERMANENT_FAILURE}
+ * </ul>
+ *
+ * @return {@code Map} from KeyStore alias to recovery status.
+ * @see #setRecoveryStatus
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public Map<String, Integer> getRecoveryStatus() throws InternalRecoveryServiceException {
+ try {
+ // IPC doesn't support generic Maps.
+ @SuppressWarnings("unchecked")
+ Map<String, Integer> result =
+ (Map<String, Integer>) mBinder.getRecoveryStatus(/*packageName=*/ null);
+ return result;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Specifies a set of secret types used for end-to-end keystore encryption. Knowing all of them
+ * is necessary to recover data.
+ *
+ * @param secretTypes {@link KeychainProtectionParams#TYPE_LOCKSCREEN} or {@link
+ * KeychainProtectionParams#TYPE_CUSTOM_PASSWORD}
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void setRecoverySecretTypes(
+ @NonNull @KeychainProtectionParams.UserSecretType int[] secretTypes)
+ throws InternalRecoveryServiceException {
+ try {
+ mBinder.setRecoverySecretTypes(secretTypes);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Defines a set of secret types used for end-to-end keystore encryption. Knowing all of them is
+ * necessary to generate KeychainSnapshot.
+ *
+ * @return list of recovery secret types
+ * @see KeychainSnapshot
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public @NonNull @KeychainProtectionParams.UserSecretType int[] getRecoverySecretTypes()
+ throws InternalRecoveryServiceException {
+ try {
+ return mBinder.getRecoverySecretTypes();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns a list of recovery secret types, necessary to create a pending recovery snapshot.
+ * When user enters a secret of a pending type {@link #recoverySecretAvailable} should be
+ * called.
+ *
+ * @return list of recovery secret types
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @NonNull
+ public @KeychainProtectionParams.UserSecretType int[] getPendingRecoverySecretTypes()
+ throws InternalRecoveryServiceException {
+ try {
+ return mBinder.getPendingRecoverySecretTypes();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Initializes recovery session and returns a blob with proof of recovery secrets possession.
+ * The method generates symmetric key for a session, which trusted remote device can use to
+ * return recovery key.
+ *
+ * @param verifierPublicKey Encoded {@code java.security.cert.X509Certificate} with Public key
+ * used to create the recovery blob on the source device.
+ * Keystore will verify the certificate using root of trust.
+ * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
+ * Used to limit number of guesses.
+ * @param vaultChallenge Data passed from server for this recovery session and used to prevent
+ * replay attacks
+ * @param secrets Secrets provided by user, the method only uses type and secret fields.
+ * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is
+ * encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric
+ * key and parameters necessary to identify the counter with the number of failed recovery
+ * attempts.
+ * @throws BadCertificateFormatException if the {@code verifierPublicKey} is in an incorrect
+ * format.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @NonNull public RecoveryClaim startRecoverySession(
+ @NonNull byte[] verifierPublicKey,
+ @NonNull byte[] vaultParams,
+ @NonNull byte[] vaultChallenge,
+ @NonNull List<KeychainProtectionParams> secrets)
+ throws BadCertificateFormatException, InternalRecoveryServiceException {
+ try {
+ RecoverySession recoverySession = RecoverySession.newInstance(this);
+ byte[] recoveryClaim =
+ mBinder.startRecoverySession(
+ recoverySession.getSessionId(),
+ verifierPublicKey,
+ vaultParams,
+ vaultChallenge,
+ BackwardsCompat.fromLegacyKeychainProtectionParams(secrets));
+ return new RecoveryClaim(recoverySession, recoveryClaim);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT) {
+ throw new BadCertificateFormatException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Imports keys.
+ *
+ * @param session Related recovery session, as originally created by invoking
+ * {@link #startRecoverySession(byte[], byte[], byte[], List)}.
+ * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
+ * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
+ * and session. KeyStore only uses package names from the application info in {@link
+ * WrappedApplicationKey}. Caller is responsibility to perform certificates check.
+ * @return Map from alias to raw key material.
+ * @throws SessionExpiredException if {@code session} has since been closed.
+ * @throws DecryptionFailedException if unable to decrypt the snapshot.
+ * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service.
+ */
+ public Map<String, byte[]> recoverKeys(
+ @NonNull RecoverySession session,
+ @NonNull byte[] recoveryKeyBlob,
+ @NonNull List<WrappedApplicationKey> applicationKeys)
+ throws SessionExpiredException, DecryptionFailedException,
+ InternalRecoveryServiceException {
+ try {
+ return (Map<String, byte[]>) mBinder.recoverKeys(
+ session.getSessionId(),
+ recoveryKeyBlob,
+ BackwardsCompat.fromLegacyWrappedApplicationKeys(applicationKeys));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_DECRYPTION_FAILED) {
+ throw new DecryptionFailedException(e.getMessage());
+ }
+ if (e.errorCode == ERROR_SESSION_EXPIRED) {
+ throw new SessionExpiredException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Deletes all data associated with {@code session}. Should not be invoked directly but via
+ * {@link RecoverySession#close()}.
+ *
+ * @hide
+ */
+ void closeSession(RecoverySession session) {
+ try {
+ mBinder.closeSession(session.getSessionId());
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Unexpected error trying to close session", e);
+ }
+ }
+
+ /**
+ * Generates a key called {@code alias} and loads it into the recoverable key store. Returns the
+ * raw material of the key.
+ *
+ * @param alias The key alias.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ * @throws LockScreenRequiredException if the user has not set a lock screen. This is required
+ * to generate recoverable keys, as the snapshots are encrypted using a key derived from the
+ * lock screen.
+ */
+ public byte[] generateAndStoreKey(@NonNull String alias)
+ throws InternalRecoveryServiceException, LockScreenRequiredException {
+ try {
+ return mBinder.generateAndStoreKey(alias);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_INSECURE_USER) {
+ throw new LockScreenRequiredException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Removes a key called {@code alias} from the recoverable key store.
+ *
+ * @param alias The key alias.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void removeKey(@NonNull String alias) throws InternalRecoveryServiceException {
+ try {
+ mBinder.removeKey(alias);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ private InternalRecoveryServiceException wrapUnexpectedServiceSpecificException(
+ ServiceSpecificException e) {
+ if (e.errorCode == ERROR_SERVICE_INTERNAL_ERROR) {
+ return new InternalRecoveryServiceException(e.getMessage());
+ }
+
+ // Should never happen. If it does, it's a bug, and we need to update how the method that
+ // called this throws its exceptions.
+ return new InternalRecoveryServiceException("Unexpected error code for method: "
+ + e.errorCode, e);
+ }
+}
diff --git a/android/security/keystore/RecoveryControllerException.java b/android/security/keystore/RecoveryControllerException.java
new file mode 100644
index 0000000..5b806b7
--- /dev/null
+++ b/android/security/keystore/RecoveryControllerException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 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.security.keystore;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * Base exception for errors thrown by {@link RecoveryController}.
+ *
+ * @hide
+ */
+public abstract class RecoveryControllerException extends GeneralSecurityException {
+ RecoveryControllerException() { }
+
+ RecoveryControllerException(String msg) {
+ super(msg);
+ }
+
+ public RecoveryControllerException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/android/security/keystore/RecoverySession.java b/android/security/keystore/RecoverySession.java
new file mode 100644
index 0000000..ae8d91a
--- /dev/null
+++ b/android/security/keystore/RecoverySession.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2018 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.security.keystore;
+
+import java.security.SecureRandom;
+
+/**
+ * Session to recover a {@link KeychainSnapshot} from the remote trusted hardware, initiated by a
+ * recovery agent.
+ *
+ * @hide
+ */
+public class RecoverySession implements AutoCloseable {
+
+ private static final int SESSION_ID_LENGTH_BYTES = 16;
+
+ private final String mSessionId;
+ private final RecoveryController mRecoveryController;
+
+ private RecoverySession(RecoveryController recoveryController, String sessionId) {
+ mRecoveryController = recoveryController;
+ mSessionId = sessionId;
+ }
+
+ /**
+ * A new session, started by {@code recoveryManager}.
+ */
+ static RecoverySession newInstance(RecoveryController recoveryController) {
+ return new RecoverySession(recoveryController, newSessionId());
+ }
+
+ /**
+ * Returns a new random session ID.
+ */
+ private static String newSessionId() {
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES];
+ secureRandom.nextBytes(sessionId);
+ StringBuilder sb = new StringBuilder();
+ for (byte b : sessionId) {
+ sb.append(Byte.toHexString(b, /*upperCase=*/ false));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * An internal session ID, used by the framework to match recovery claims to snapshot responses.
+ */
+ String getSessionId() {
+ return mSessionId;
+ }
+
+ @Override
+ public void close() {
+ mRecoveryController.closeSession(this);
+ }
+}
diff --git a/android/os/Seccomp.java b/android/security/keystore/SessionExpiredException.java
similarity index 63%
rename from android/os/Seccomp.java
rename to android/security/keystore/SessionExpiredException.java
index f14e93f..f13e206 100644
--- a/android/os/Seccomp.java
+++ b/android/security/keystore/SessionExpiredException.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright (C) 2018 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.
@@ -14,11 +14,15 @@
* limitations under the License.
*/
-package android.os;
+package android.security.keystore;
/**
+ * Error thrown when attempting to use a {@link RecoverySession} that has since expired.
+ *
* @hide
*/
-public final class Seccomp {
- public static final native void setPolicy();
+public class SessionExpiredException extends RecoveryControllerException {
+ public SessionExpiredException(String msg) {
+ super(msg);
+ }
}
diff --git a/android/arch/lifecycle/LifecycleActivity.java b/android/security/keystore/StrongBoxUnavailableException.java
similarity index 70%
rename from android/arch/lifecycle/LifecycleActivity.java
rename to android/security/keystore/StrongBoxUnavailableException.java
index 26bd508..ad41a58 100644
--- a/android/arch/lifecycle/LifecycleActivity.java
+++ b/android/security/keystore/StrongBoxUnavailableException.java
@@ -14,13 +14,15 @@
* limitations under the License.
*/
-package android.arch.lifecycle;
+package android.security.keystore;
-import android.support.v4.app.FragmentActivity;
+import java.security.ProviderException;
/**
- * @deprecated Use {@code android.support.v7.app.AppCompatActivity} instead of this class.
+ * Indicates that an operation could not be performed because the requested security hardware
+ * is not available.
*/
-@Deprecated
-public class LifecycleActivity extends FragmentActivity {
+public class StrongBoxUnavailableException extends ProviderException {
+
}
+
diff --git a/android/security/keystore/WrappedApplicationKey.java b/android/security/keystore/WrappedApplicationKey.java
new file mode 100644
index 0000000..522bb95
--- /dev/null
+++ b/android/security/keystore/WrappedApplicationKey.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2017 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.security.keystore;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Helper class with data necessary recover a single application key, given a recovery key.
+ *
+ * <ul>
+ * <li>Alias - Keystore alias of the key.
+ * <li>Encrypted key material.
+ * </ul>
+ *
+ * Note that Application info is not included. Recovery Agent can only make its own keys
+ * recoverable.
+ *
+ * @hide
+ */
+public final class WrappedApplicationKey implements Parcelable {
+ private String mAlias;
+ // The only supported format is AES-256 symmetric key.
+ private byte[] mEncryptedKeyMaterial;
+
+ /**
+ * Builder for creating {@link WrappedApplicationKey}.
+ */
+ public static class Builder {
+ private WrappedApplicationKey mInstance = new WrappedApplicationKey();
+
+ /**
+ * Sets Application-specific alias of the key.
+ *
+ * @param alias The alias.
+ * @return This builder.
+ */
+ public Builder setAlias(@NonNull String alias) {
+ mInstance.mAlias = alias;
+ return this;
+ }
+
+ /**
+ * Sets key material encrypted by recovery key.
+ *
+ * @param encryptedKeyMaterial The key material
+ * @return This builder
+ */
+
+ public Builder setEncryptedKeyMaterial(@NonNull byte[] encryptedKeyMaterial) {
+ mInstance.mEncryptedKeyMaterial = encryptedKeyMaterial;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link WrappedApplicationKey} instance.
+ *
+ * @return new instance
+ * @throws NullPointerException if some required fields were not set.
+ */
+ @NonNull public WrappedApplicationKey build() {
+ Preconditions.checkNotNull(mInstance.mAlias);
+ Preconditions.checkNotNull(mInstance.mEncryptedKeyMaterial);
+ return mInstance;
+ }
+ }
+
+ private WrappedApplicationKey() {
+
+ }
+
+ /**
+ * Deprecated - consider using Builder.
+ * @hide
+ */
+ public WrappedApplicationKey(@NonNull String alias, @NonNull byte[] encryptedKeyMaterial) {
+ mAlias = Preconditions.checkNotNull(alias);
+ mEncryptedKeyMaterial = Preconditions.checkNotNull(encryptedKeyMaterial);
+ }
+
+ /**
+ * Application-specific alias of the key.
+ *
+ * @see java.security.KeyStore.aliases
+ */
+ public @NonNull String getAlias() {
+ return mAlias;
+ }
+
+ /** Key material encrypted by recovery key. */
+ public @NonNull byte[] getEncryptedKeyMaterial() {
+ return mEncryptedKeyMaterial;
+ }
+
+ public static final Parcelable.Creator<WrappedApplicationKey> CREATOR =
+ new Parcelable.Creator<WrappedApplicationKey>() {
+ public WrappedApplicationKey createFromParcel(Parcel in) {
+ return new WrappedApplicationKey(in);
+ }
+
+ public WrappedApplicationKey[] newArray(int length) {
+ return new WrappedApplicationKey[length];
+ }
+ };
+
+ /**
+ * @hide
+ */
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mAlias);
+ out.writeByteArray(mEncryptedKeyMaterial);
+ }
+
+ /**
+ * @hide
+ */
+ protected WrappedApplicationKey(Parcel in) {
+ mAlias = in.readString();
+ mEncryptedKeyMaterial = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/security/keystore/WrappedKeyEntry.java b/android/security/keystore/WrappedKeyEntry.java
new file mode 100644
index 0000000..a8f4afe
--- /dev/null
+++ b/android/security/keystore/WrappedKeyEntry.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 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.security.keystore;
+
+import java.security.KeyStore.Entry;
+import java.security.spec.AlgorithmParameterSpec;
+
+/**
+ * An {@link Entry} that holds a wrapped key.
+ */
+public class WrappedKeyEntry implements Entry {
+
+ private final byte[] mWrappedKeyBytes;
+ private final String mWrappingKeyAlias;
+ private final String mTransformation;
+ private final AlgorithmParameterSpec mAlgorithmParameterSpec;
+
+ public WrappedKeyEntry(byte[] wrappedKeyBytes, String wrappingKeyAlias, String transformation,
+ AlgorithmParameterSpec algorithmParameterSpec) {
+ mWrappedKeyBytes = wrappedKeyBytes;
+ mWrappingKeyAlias = wrappingKeyAlias;
+ mTransformation = transformation;
+ mAlgorithmParameterSpec = algorithmParameterSpec;
+ }
+
+ public byte[] getWrappedKeyBytes() {
+ return mWrappedKeyBytes;
+ }
+
+ public String getWrappingKeyAlias() {
+ return mWrappingKeyAlias;
+ }
+
+ public String getTransformation() {
+ return mTransformation;
+ }
+
+ public AlgorithmParameterSpec getAlgorithmParameterSpec() {
+ return mAlgorithmParameterSpec;
+ }
+}
diff --git a/android/os/Seccomp.java b/android/security/keystore/recovery/BadCertificateFormatException.java
similarity index 61%
copy from android/os/Seccomp.java
copy to android/security/keystore/recovery/BadCertificateFormatException.java
index f14e93f..e0781a5 100644
--- a/android/os/Seccomp.java
+++ b/android/security/keystore/recovery/BadCertificateFormatException.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright (C) 2018 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.
@@ -14,11 +14,16 @@
* limitations under the License.
*/
-package android.os;
+package android.security.keystore.recovery;
/**
+ * Error thrown when the recovery agent supplies an invalid X509 certificate.
+ *
* @hide
+ * Deprecated
*/
-public final class Seccomp {
- public static final native void setPolicy();
+public class BadCertificateFormatException extends RecoveryControllerException {
+ public BadCertificateFormatException(String msg) {
+ super(msg);
+ }
}
diff --git a/android/security/keystore/recovery/DecryptionFailedException.java b/android/security/keystore/recovery/DecryptionFailedException.java
new file mode 100644
index 0000000..af00e05
--- /dev/null
+++ b/android/security/keystore/recovery/DecryptionFailedException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 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.security.keystore.recovery;
+
+import android.annotation.SystemApi;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * Error thrown when decryption failed, due to an agent error. i.e., using the incorrect key,
+ * trying to decrypt garbage data, trying to decrypt data that has somehow been corrupted, etc.
+ *
+ * @hide
+ */
+@SystemApi
+public class DecryptionFailedException extends GeneralSecurityException {
+ public DecryptionFailedException(String msg) {
+ super(msg);
+ }
+}
diff --git a/android/security/keystore/recovery/InternalRecoveryServiceException.java b/android/security/keystore/recovery/InternalRecoveryServiceException.java
new file mode 100644
index 0000000..218d26e
--- /dev/null
+++ b/android/security/keystore/recovery/InternalRecoveryServiceException.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2018 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.security.keystore.recovery;
+
+import android.annotation.SystemApi;
+
+import java.security.GeneralSecurityException;
+/**
+ * An error thrown when something went wrong internally in the recovery service.
+ *
+ * <p>This is an unexpected error, and indicates a problem with the service itself, rather than the
+ * caller having performed some kind of illegal action.
+ *
+ * @hide
+ */
+@SystemApi
+public class InternalRecoveryServiceException extends GeneralSecurityException {
+ public InternalRecoveryServiceException(String msg) {
+ super(msg);
+ }
+
+ public InternalRecoveryServiceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/android/security/keystore/recovery/KeyChainProtectionParams.java b/android/security/keystore/recovery/KeyChainProtectionParams.java
new file mode 100644
index 0000000..a43952a
--- /dev/null
+++ b/android/security/keystore/recovery/KeyChainProtectionParams.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2017 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.security.keystore.recovery;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+
+/**
+ * A {@link KeyChainSnapshot} is protected with a key derived from the user's lock screen. This
+ * class wraps all the data necessary to derive the same key on a recovering device:
+ *
+ * <ul>
+ * <li>UI parameters for the user's lock screen - so that if e.g., the user was using a pattern,
+ * the recovering device can display the pattern UI to the user when asking them to enter
+ * the lock screen from their previous device.
+ * <li>The algorithm used to derive a key from the user's lock screen, e.g. SHA-256 with a salt.
+ * </ul>
+ *
+ * <p>As such, this data is sent along with the {@link KeyChainSnapshot} when syncing the current
+ * version of the keychain.
+ *
+ * <p>For now, the recoverable keychain only supports a single layer of protection, which is the
+ * user's lock screen. In the future, the keychain will support multiple layers of protection
+ * (e.g. an additional keychain password, along with the lock screen).
+ *
+ * @hide
+ */
+@SystemApi
+public final class KeyChainProtectionParams implements Parcelable {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"TYPE_"}, value = {TYPE_LOCKSCREEN, TYPE_CUSTOM_PASSWORD})
+ public @interface UserSecretType {
+ }
+
+ /**
+ * Lockscreen secret is required to recover KeyStore.
+ */
+ public static final int TYPE_LOCKSCREEN = 100;
+
+ /**
+ * Custom passphrase, unrelated to lock screen, is required to recover KeyStore.
+ */
+ public static final int TYPE_CUSTOM_PASSWORD = 101;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"UI_FORMAT_"}, value = {UI_FORMAT_PIN, UI_FORMAT_PASSWORD, UI_FORMAT_PATTERN})
+ public @interface LockScreenUiFormat {
+ }
+
+ /**
+ * Pin with digits only.
+ */
+ public static final int UI_FORMAT_PIN = 1;
+
+ /**
+ * Password. String with latin-1 characters only.
+ */
+ public static final int UI_FORMAT_PASSWORD = 2;
+
+ /**
+ * Pattern with 3 by 3 grid.
+ */
+ public static final int UI_FORMAT_PATTERN = 3;
+
+ @UserSecretType
+ private Integer mUserSecretType;
+
+ @LockScreenUiFormat
+ private Integer mLockScreenUiFormat;
+
+ /**
+ * Parameters of the key derivation function, including algorithm, difficulty, salt.
+ */
+ private KeyDerivationParams mKeyDerivationParams;
+ private byte[] mSecret; // Derived from user secret. The field must have limited visibility.
+
+ /**
+ * @param secret Constructor creates a reference to the secret. Caller must use
+ * @link {#clearSecret} to overwrite its value in memory.
+ * @hide
+ */
+ public KeyChainProtectionParams(@UserSecretType int userSecretType,
+ @LockScreenUiFormat int lockScreenUiFormat,
+ @NonNull KeyDerivationParams keyDerivationParams,
+ @NonNull byte[] secret) {
+ mUserSecretType = userSecretType;
+ mLockScreenUiFormat = lockScreenUiFormat;
+ mKeyDerivationParams = Preconditions.checkNotNull(keyDerivationParams);
+ mSecret = Preconditions.checkNotNull(secret);
+ }
+
+ private KeyChainProtectionParams() {
+
+ }
+
+ /**
+ * @see TYPE_LOCKSCREEN
+ * @see TYPE_CUSTOM_PASSWORD
+ */
+ public @UserSecretType int getUserSecretType() {
+ return mUserSecretType;
+ }
+
+ /**
+ * Specifies UX shown to user during recovery.
+ * Default value is {@code UI_FORMAT_LOCKSCREEN}
+ *
+ * @see UI_FORMAT_PIN
+ * @see UI_FORMAT_PASSWORD
+ * @see UI_FORMAT_PATTERN
+ */
+ public @LockScreenUiFormat int getLockScreenUiFormat() {
+ return mLockScreenUiFormat;
+ }
+
+ /**
+ * Specifies function used to derive symmetric key from user input
+ * Format is defined in separate util class.
+ */
+ public @NonNull KeyDerivationParams getKeyDerivationParams() {
+ return mKeyDerivationParams;
+ }
+
+ /**
+ * Secret derived from user input.
+ * Default value is empty array
+ *
+ * @return secret or empty array
+ */
+ public @NonNull byte[] getSecret() {
+ return mSecret;
+ }
+
+ /**
+ * Builder for creating {@link KeyChainProtectionParams}.
+ */
+ public static class Builder {
+ private KeyChainProtectionParams mInstance = new KeyChainProtectionParams();
+
+ /**
+ * Sets user secret type.
+ *
+ * @see TYPE_LOCKSCREEN
+ * @see TYPE_CUSTOM_PASSWORD
+ * @param userSecretType The secret type
+ * @return This builder.
+ */
+ public Builder setUserSecretType(@UserSecretType int userSecretType) {
+ mInstance.mUserSecretType = userSecretType;
+ return this;
+ }
+
+ /**
+ * Sets UI format.
+ *
+ * @see UI_FORMAT_PIN
+ * @see UI_FORMAT_PASSWORD
+ * @see UI_FORMAT_PATTERN
+ * @param lockScreenUiFormat The UI format
+ * @return This builder.
+ */
+ public Builder setLockScreenUiFormat(@LockScreenUiFormat int lockScreenUiFormat) {
+ mInstance.mLockScreenUiFormat = lockScreenUiFormat;
+ return this;
+ }
+
+ /**
+ * Sets parameters of the key derivation function.
+ *
+ * @param keyDerivationParams Key derivation Params
+ * @return This builder.
+ */
+ public Builder setKeyDerivationParams(@NonNull KeyDerivationParams
+ keyDerivationParams) {
+ mInstance.mKeyDerivationParams = keyDerivationParams;
+ return this;
+ }
+
+ /**
+ * Secret derived from user input, or empty array.
+ *
+ * @param secret The secret.
+ * @return This builder.
+ */
+ public Builder setSecret(@NonNull byte[] secret) {
+ mInstance.mSecret = secret;
+ return this;
+ }
+
+
+ /**
+ * Creates a new {@link KeyChainProtectionParams} instance.
+ * The instance will include default values, if {@link setSecret}
+ * or {@link setUserSecretType} were not called.
+ *
+ * @return new instance
+ * @throws NullPointerException if some required fields were not set.
+ */
+ @NonNull public KeyChainProtectionParams build() {
+ if (mInstance.mUserSecretType == null) {
+ mInstance.mUserSecretType = TYPE_LOCKSCREEN;
+ }
+ Preconditions.checkNotNull(mInstance.mLockScreenUiFormat);
+ Preconditions.checkNotNull(mInstance.mKeyDerivationParams);
+ if (mInstance.mSecret == null) {
+ mInstance.mSecret = new byte[]{};
+ }
+ return mInstance;
+ }
+ }
+
+ /**
+ * Removes secret from memory than object is no longer used.
+ * Since finalizer call is not reliable, please use @link {#clearSecret} directly.
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ clearSecret();
+ super.finalize();
+ }
+
+ /**
+ * Fills mSecret with zeroes.
+ */
+ public void clearSecret() {
+ Arrays.fill(mSecret, (byte) 0);
+ }
+
+ public static final Parcelable.Creator<KeyChainProtectionParams> CREATOR =
+ new Parcelable.Creator<KeyChainProtectionParams>() {
+ public KeyChainProtectionParams createFromParcel(Parcel in) {
+ return new KeyChainProtectionParams(in);
+ }
+
+ public KeyChainProtectionParams[] newArray(int length) {
+ return new KeyChainProtectionParams[length];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mUserSecretType);
+ out.writeInt(mLockScreenUiFormat);
+ out.writeTypedObject(mKeyDerivationParams, flags);
+ out.writeByteArray(mSecret);
+ }
+
+ /**
+ * @hide
+ */
+ protected KeyChainProtectionParams(Parcel in) {
+ mUserSecretType = in.readInt();
+ mLockScreenUiFormat = in.readInt();
+ mKeyDerivationParams = in.readTypedObject(KeyDerivationParams.CREATOR);
+ mSecret = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/security/keystore/recovery/KeyChainSnapshot.java b/android/security/keystore/recovery/KeyChainSnapshot.java
new file mode 100644
index 0000000..df535ed
--- /dev/null
+++ b/android/security/keystore/recovery/KeyChainSnapshot.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2017 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.security.keystore.recovery;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+
+/**
+ * A snapshot of a version of the keystore. Two events can trigger the generation of a new snapshot:
+ *
+ * <ul>
+ * <li>The user's lock screen changes. (A key derived from the user's lock screen is used to
+ * protected the keychain, which is why this forces a new snapshot.)
+ * <li>A key is added to or removed from the recoverable keychain.
+ * </ul>
+ *
+ * <p>The snapshot data is also encrypted with the remote trusted hardware's public key, so even
+ * the recovery agent itself should not be able to decipher the data. The recovery agent sends an
+ * instance of this to the remote trusted hardware whenever a new snapshot is generated. During a
+ * recovery flow, the recovery agent retrieves a snapshot from the remote trusted hardware. It then
+ * sends it to the framework, where it is decrypted using the user's lock screen from their previous
+ * device.
+ *
+ * @hide
+ */
+@SystemApi
+public final class KeyChainSnapshot implements Parcelable {
+ private static final int DEFAULT_MAX_ATTEMPTS = 10;
+ private static final long DEFAULT_COUNTER_ID = 1L;
+
+ private int mSnapshotVersion;
+ private int mMaxAttempts = DEFAULT_MAX_ATTEMPTS;
+ private long mCounterId = DEFAULT_COUNTER_ID;
+ private byte[] mServerParams;
+ private byte[] mPublicKey;
+ private List<KeyChainProtectionParams> mKeyChainProtectionParams;
+ private List<WrappedApplicationKey> mEntryRecoveryData;
+ private byte[] mEncryptedRecoveryKeyBlob;
+
+ /**
+ * @hide
+ * Deprecated, consider using builder.
+ */
+ public KeyChainSnapshot(
+ int snapshotVersion,
+ @NonNull List<KeyChainProtectionParams> keyChainProtectionParams,
+ @NonNull List<WrappedApplicationKey> wrappedApplicationKeys,
+ @NonNull byte[] encryptedRecoveryKeyBlob) {
+ mSnapshotVersion = snapshotVersion;
+ mKeyChainProtectionParams =
+ Preconditions.checkCollectionElementsNotNull(keyChainProtectionParams,
+ "KeyChainProtectionParams");
+ mEntryRecoveryData = Preconditions.checkCollectionElementsNotNull(wrappedApplicationKeys,
+ "wrappedApplicationKeys");
+ mEncryptedRecoveryKeyBlob = Preconditions.checkNotNull(encryptedRecoveryKeyBlob);
+ }
+
+ private KeyChainSnapshot() {
+
+ }
+
+ /**
+ * Snapshot version for given account. It is incremented when user secret or list of application
+ * keys changes.
+ */
+ public int getSnapshotVersion() {
+ return mSnapshotVersion;
+ }
+
+ /**
+ * Number of user secret guesses allowed during Keychain recovery.
+ */
+ public int getMaxAttempts() {
+ return mMaxAttempts;
+ }
+
+ /**
+ * CounterId which is rotated together with user secret.
+ */
+ public long getCounterId() {
+ return mCounterId;
+ }
+
+ /**
+ * Server parameters.
+ */
+ public @NonNull byte[] getServerParams() {
+ return mServerParams;
+ }
+
+ /**
+ * Public key used to encrypt {@code encryptedRecoveryKeyBlob}.
+ *
+ * See implementation for binary key format
+ */
+ // TODO: document key format.
+ public @NonNull byte[] getTrustedHardwarePublicKey() {
+ return mPublicKey;
+ }
+
+ /**
+ * UI and key derivation parameters. Note that combination of secrets may be used.
+ */
+ public @NonNull List<KeyChainProtectionParams> getKeyChainProtectionParams() {
+ return mKeyChainProtectionParams;
+ }
+
+ /**
+ * List of application keys, with key material encrypted by
+ * the recovery key ({@link #getEncryptedRecoveryKeyBlob}).
+ */
+ public @NonNull List<WrappedApplicationKey> getWrappedApplicationKeys() {
+ return mEntryRecoveryData;
+ }
+
+ /**
+ * Recovery key blob, encrypted by user secret and recovery service public key.
+ */
+ public @NonNull byte[] getEncryptedRecoveryKeyBlob() {
+ return mEncryptedRecoveryKeyBlob;
+ }
+
+ public static final Creator<KeyChainSnapshot> CREATOR =
+ new Creator<KeyChainSnapshot>() {
+ public KeyChainSnapshot createFromParcel(Parcel in) {
+ return new KeyChainSnapshot(in);
+ }
+
+ public KeyChainSnapshot[] newArray(int length) {
+ return new KeyChainSnapshot[length];
+ }
+ };
+
+ /**
+ * Builder for creating {@link KeyChainSnapshot}.
+ * @hide
+ */
+ public static class Builder {
+ private KeyChainSnapshot mInstance = new KeyChainSnapshot();
+
+ /**
+ * Snapshot version for given account.
+ *
+ * @param snapshotVersion The snapshot version
+ * @return This builder.
+ */
+ public Builder setSnapshotVersion(int snapshotVersion) {
+ mInstance.mSnapshotVersion = snapshotVersion;
+ return this;
+ }
+
+ /**
+ * Sets the number of user secret guesses allowed during Keychain recovery.
+ *
+ * @param maxAttempts The maximum number of guesses.
+ * @return This builder.
+ */
+ public Builder setMaxAttempts(int maxAttempts) {
+ mInstance.mMaxAttempts = maxAttempts;
+ return this;
+ }
+
+ /**
+ * Sets counter id.
+ *
+ * @param counterId The counter id.
+ * @return This builder.
+ */
+ public Builder setCounterId(long counterId) {
+ mInstance.mCounterId = counterId;
+ return this;
+ }
+
+ /**
+ * Sets server parameters.
+ *
+ * @param serverParams The server parameters
+ * @return This builder.
+ */
+ public Builder setServerParams(byte[] serverParams) {
+ mInstance.mServerParams = serverParams;
+ return this;
+ }
+
+ /**
+ * Sets public key used to encrypt recovery blob.
+ *
+ * @param publicKey The public key
+ * @return This builder.
+ */
+ public Builder setTrustedHardwarePublicKey(byte[] publicKey) {
+ mInstance.mPublicKey = publicKey;
+ return this;
+ }
+
+ /**
+ * Sets UI and key derivation parameters
+ *
+ * @param recoveryMetadata The UI and key derivation parameters
+ * @return This builder.
+ */
+ public Builder setKeyChainProtectionParams(
+ @NonNull List<KeyChainProtectionParams> recoveryMetadata) {
+ mInstance.mKeyChainProtectionParams = recoveryMetadata;
+ return this;
+ }
+
+ /**
+ * List of application keys.
+ *
+ * @param entryRecoveryData List of application keys
+ * @return This builder.
+ */
+ public Builder setWrappedApplicationKeys(List<WrappedApplicationKey> entryRecoveryData) {
+ mInstance.mEntryRecoveryData = entryRecoveryData;
+ return this;
+ }
+
+ /**
+ * Sets recovery key blob
+ *
+ * @param encryptedRecoveryKeyBlob The recovery key blob.
+ * @return This builder.
+ */
+ public Builder setEncryptedRecoveryKeyBlob(@NonNull byte[] encryptedRecoveryKeyBlob) {
+ mInstance.mEncryptedRecoveryKeyBlob = encryptedRecoveryKeyBlob;
+ return this;
+ }
+
+
+ /**
+ * Creates a new {@link KeyChainSnapshot} instance.
+ *
+ * @return new instance
+ * @throws NullPointerException if some required fields were not set.
+ */
+ @NonNull public KeyChainSnapshot build() {
+ Preconditions.checkCollectionElementsNotNull(mInstance.mKeyChainProtectionParams,
+ "recoveryMetadata");
+ Preconditions.checkCollectionElementsNotNull(mInstance.mEntryRecoveryData,
+ "entryRecoveryData");
+ Preconditions.checkNotNull(mInstance.mEncryptedRecoveryKeyBlob);
+ Preconditions.checkNotNull(mInstance.mServerParams);
+ Preconditions.checkNotNull(mInstance.mPublicKey);
+ return mInstance;
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mSnapshotVersion);
+ out.writeTypedList(mKeyChainProtectionParams);
+ out.writeByteArray(mEncryptedRecoveryKeyBlob);
+ out.writeTypedList(mEntryRecoveryData);
+ out.writeInt(mMaxAttempts);
+ out.writeLong(mCounterId);
+ out.writeByteArray(mServerParams);
+ out.writeByteArray(mPublicKey);
+ }
+
+ /**
+ * @hide
+ */
+ protected KeyChainSnapshot(Parcel in) {
+ mSnapshotVersion = in.readInt();
+ mKeyChainProtectionParams = in.createTypedArrayList(KeyChainProtectionParams.CREATOR);
+ mEncryptedRecoveryKeyBlob = in.createByteArray();
+ mEntryRecoveryData = in.createTypedArrayList(WrappedApplicationKey.CREATOR);
+ mMaxAttempts = in.readInt();
+ mCounterId = in.readLong();
+ mServerParams = in.createByteArray();
+ mPublicKey = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/security/recoverablekeystore/KeyDerivationParameters.java b/android/security/keystore/recovery/KeyDerivationParams.java
similarity index 66%
copy from android/security/recoverablekeystore/KeyDerivationParameters.java
copy to android/security/keystore/recovery/KeyDerivationParams.java
index 978e60e..fc909a0 100644
--- a/android/security/recoverablekeystore/KeyDerivationParameters.java
+++ b/android/security/keystore/recovery/KeyDerivationParams.java
@@ -14,13 +14,15 @@
* limitations under the License.
*/
-package android.security.recoverablekeystore;
+package android.security.keystore.recovery;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
+
import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
@@ -28,21 +30,18 @@
/**
* Collection of parameters which define a key derivation function.
- * Supports
+ * Currently only supports salted SHA-256
*
- * <ul>
- * <li>SHA256
- * <li>Argon2id
- * </ul>
* @hide
*/
-public final class KeyDerivationParameters implements Parcelable {
+@SystemApi
+public final class KeyDerivationParams implements Parcelable {
private final int mAlgorithm;
private byte[] mSalt;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({ALGORITHM_SHA256, ALGORITHM_ARGON2ID})
+ @IntDef(prefix = {"ALGORITHM_"}, value = {ALGORITHM_SHA256, ALGORITHM_ARGON2ID})
public @interface KeyDerivationAlgorithm {
}
@@ -53,6 +52,7 @@
/**
* Argon2ID
+ * @hide
*/
// TODO: add Argon2ID support.
public static final int ALGORITHM_ARGON2ID = 2;
@@ -60,11 +60,15 @@
/**
* Creates instance of the class to to derive key using salted SHA256 hash.
*/
- public static KeyDerivationParameters createSHA256Parameters(@NonNull byte[] salt) {
- return new KeyDerivationParameters(ALGORITHM_SHA256, salt);
+ public static KeyDerivationParams createSha256Params(@NonNull byte[] salt) {
+ return new KeyDerivationParams(ALGORITHM_SHA256, salt);
}
- private KeyDerivationParameters(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) {
+ /**
+ * @hide
+ */
+ // TODO: Make private once legacy API is removed
+ public KeyDerivationParams(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) {
mAlgorithm = algorithm;
mSalt = Preconditions.checkNotNull(salt);
}
@@ -83,14 +87,14 @@
return mSalt;
}
- public static final Parcelable.Creator<KeyDerivationParameters> CREATOR =
- new Parcelable.Creator<KeyDerivationParameters>() {
- public KeyDerivationParameters createFromParcel(Parcel in) {
- return new KeyDerivationParameters(in);
+ public static final Parcelable.Creator<KeyDerivationParams> CREATOR =
+ new Parcelable.Creator<KeyDerivationParams>() {
+ public KeyDerivationParams createFromParcel(Parcel in) {
+ return new KeyDerivationParams(in);
}
- public KeyDerivationParameters[] newArray(int length) {
- return new KeyDerivationParameters[length];
+ public KeyDerivationParams[] newArray(int length) {
+ return new KeyDerivationParams[length];
}
};
@@ -100,7 +104,10 @@
out.writeByteArray(mSalt);
}
- protected KeyDerivationParameters(Parcel in) {
+ /**
+ * @hide
+ */
+ protected KeyDerivationParams(Parcel in) {
mAlgorithm = in.readInt();
mSalt = in.createByteArray();
}
diff --git a/android/security/keystore/recovery/LockScreenRequiredException.java b/android/security/keystore/recovery/LockScreenRequiredException.java
new file mode 100644
index 0000000..0062d29
--- /dev/null
+++ b/android/security/keystore/recovery/LockScreenRequiredException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 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.security.keystore.recovery;
+
+import android.annotation.SystemApi;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * Error thrown when trying to generate keys for a profile that has no lock screen set.
+ *
+ * <p>A lock screen must be set, as the lock screen is used to encrypt the snapshot.
+ *
+ * @hide
+ */
+@SystemApi
+public class LockScreenRequiredException extends GeneralSecurityException {
+ public LockScreenRequiredException(String msg) {
+ super(msg);
+ }
+}
diff --git a/android/security/keystore/recovery/RecoveryClaim.java b/android/security/keystore/recovery/RecoveryClaim.java
new file mode 100644
index 0000000..45c6b4f
--- /dev/null
+++ b/android/security/keystore/recovery/RecoveryClaim.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 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.security.keystore.recovery;
+
+/**
+ * An attempt to recover a keychain protected by remote secure hardware.
+ *
+ * @hide
+ * Deprecated
+ */
+public class RecoveryClaim {
+
+ private final RecoverySession mRecoverySession;
+ private final byte[] mClaimBytes;
+
+ RecoveryClaim(RecoverySession recoverySession, byte[] claimBytes) {
+ mRecoverySession = recoverySession;
+ mClaimBytes = claimBytes;
+ }
+
+ /**
+ * Returns the session associated with the recovery attempt. This is used to match the symmetric
+ * key, which remains internal to the framework, for decrypting the claim response.
+ *
+ * @return The session data.
+ */
+ public RecoverySession getRecoverySession() {
+ return mRecoverySession;
+ }
+
+ /**
+ * Returns the encrypted claim's bytes.
+ *
+ * <p>This should be sent by the recovery agent to the remote secure hardware, which will use
+ * it to decrypt the keychain, before sending it re-encrypted with the session's symmetric key
+ * to the device.
+ */
+ public byte[] getClaimBytes() {
+ return mClaimBytes;
+ }
+}
diff --git a/android/security/keystore/recovery/RecoveryController.java b/android/security/keystore/recovery/RecoveryController.java
new file mode 100644
index 0000000..71a36f1
--- /dev/null
+++ b/android/security/keystore/recovery/RecoveryController.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright (C) 2017 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.security.keystore.recovery;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
+
+import com.android.internal.widget.ILockSettings;
+
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An assistant for generating {@link javax.crypto.SecretKey} instances that can be recovered by
+ * other Android devices belonging to the user. The exported keychain is protected by the user's
+ * lock screen.
+ *
+ * <p>The RecoveryController must be paired with a recovery agent. The recovery agent is responsible
+ * for transporting the keychain to remote trusted hardware. This hardware must prevent brute force
+ * attempts against the user's lock screen by limiting the number of allowed guesses (to, e.g., 10).
+ * After that number of incorrect guesses, the trusted hardware no longer allows access to the
+ * key chain.
+ *
+ * <p>For now only the recovery agent itself is able to create keys, so it is expected that the
+ * recovery agent is itself the system app.
+ *
+ * <p>A recovery agent requires the privileged permission
+ * {@code android.Manifest.permission#RECOVER_KEYSTORE}.
+ *
+ * @hide
+ */
+@SystemApi
+public class RecoveryController {
+ private static final String TAG = "RecoveryController";
+
+ /** Key has been successfully synced. */
+ public static final int RECOVERY_STATUS_SYNCED = 0;
+ /** Waiting for recovery agent to sync the key. */
+ public static final int RECOVERY_STATUS_SYNC_IN_PROGRESS = 1;
+ /** Recovery account is not available. */
+ public static final int RECOVERY_STATUS_MISSING_ACCOUNT = 2;
+ /** Key cannot be synced. */
+ public static final int RECOVERY_STATUS_PERMANENT_FAILURE = 3;
+
+ /**
+ * Failed because no snapshot is yet pending to be synced for the user.
+ *
+ * @hide
+ */
+ public static final int ERROR_NO_SNAPSHOT_PENDING = 21;
+
+ /**
+ * Failed due to an error internal to the recovery service. This is unexpected and indicates
+ * either a problem with the logic in the service, or a problem with a dependency of the
+ * service (such as AndroidKeyStore).
+ *
+ * @hide
+ */
+ public static final int ERROR_SERVICE_INTERNAL_ERROR = 22;
+
+ /**
+ * Failed because the user does not have a lock screen set.
+ *
+ * @hide
+ */
+ public static final int ERROR_INSECURE_USER = 23;
+
+ /**
+ * Error thrown when attempting to use a recovery session that has since been closed.
+ *
+ * @hide
+ */
+ public static final int ERROR_SESSION_EXPIRED = 24;
+
+ /**
+ * Failed because the provided certificate was not a valid X509 certificate.
+ *
+ * @hide
+ */
+ public static final int ERROR_BAD_CERTIFICATE_FORMAT = 25;
+
+ /**
+ * Error thrown if decryption failed. This might be because the tag is wrong, the key is wrong,
+ * the data has become corrupted, the data has been tampered with, etc.
+ *
+ * @hide
+ */
+ public static final int ERROR_DECRYPTION_FAILED = 26;
+
+
+ private final ILockSettings mBinder;
+
+ private RecoveryController(ILockSettings binder) {
+ mBinder = binder;
+ }
+
+ /**
+ * Internal method used by {@code RecoverySession}.
+ *
+ * @hide
+ */
+ ILockSettings getBinder() {
+ return mBinder;
+ }
+
+ /**
+ * Gets a new instance of the class.
+ */
+ public static RecoveryController getInstance(Context context) {
+ ILockSettings lockSettings =
+ ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings"));
+ return new RecoveryController(lockSettings);
+ }
+
+ /**
+ * Initializes key recovery service for the calling application. RecoveryController
+ * randomly chooses one of the keys from the list and keeps it to use for future key export
+ * operations. Collection of all keys in the list must be signed by the provided {@code
+ * rootCertificateAlias}, which must also be present in the list of root certificates
+ * preinstalled on the device. The random selection allows RecoveryController to select
+ * which of a set of remote recovery service devices will be used.
+ *
+ * <p>In addition, RecoveryController enforces a delay of three months between
+ * consecutive initialization attempts, to limit the ability of an attacker to often switch
+ * remote recovery devices and significantly increase number of recovery attempts.
+ *
+ * @param rootCertificateAlias alias of a root certificate preinstalled on the device
+ * @param signedPublicKeyList binary blob a list of X509 certificates and signature
+ * @throws CertificateException if the {@code signedPublicKeyList} is in a bad format.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ public void initRecoveryService(
+ @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList)
+ throws CertificateException, InternalRecoveryServiceException {
+ try {
+ mBinder.initRecoveryService(rootCertificateAlias, signedPublicKeyList);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT) {
+ throw new CertificateException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns data necessary to store all recoverable keys. Key material is
+ * encrypted with user secret and recovery public key.
+ *
+ * @return Data necessary to recover keystore.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ public @NonNull KeyChainSnapshot getRecoveryData()
+ throws InternalRecoveryServiceException {
+ try {
+ return mBinder.getRecoveryData(/*account=*/ new byte[]{});
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_NO_SNAPSHOT_PENDING) {
+ return null;
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Sets a listener which notifies recovery agent that new recovery snapshot is available. {@link
+ * #getRecoveryData} can be used to get the snapshot. Note that every recovery agent can have at
+ * most one registered listener at any time.
+ *
+ * @param intent triggered when new snapshot is available. Unregisters listener if the value is
+ * {@code null}.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent)
+ throws InternalRecoveryServiceException {
+ try {
+ mBinder.setSnapshotCreatedPendingIntent(intent);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Server parameters used to generate new recovery key blobs. This value will be included in
+ * {@code KeyChainSnapshot.getEncryptedRecoveryKeyBlob()}. The same value must be included
+ * in vaultParams {@link #startRecoverySession}
+ *
+ * @param serverParams included in recovery key blob.
+ * @see #getRecoveryData
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ public void setServerParams(byte[] serverParams) throws InternalRecoveryServiceException {
+ try {
+ mBinder.setServerParams(serverParams);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Gets aliases of recoverable keys for the application.
+ *
+ * @param packageName which recoverable keys' aliases will be returned.
+ *
+ * @return {@code List} of all aliases.
+ */
+ public List<String> getAliases(@Nullable String packageName)
+ throws InternalRecoveryServiceException {
+ try {
+ // TODO: update aidl
+ Map<String, Integer> allStatuses = mBinder.getRecoveryStatus(packageName);
+ return new ArrayList<>(allStatuses.keySet());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Updates recovery status for given key. It is used to notify keystore that key was
+ * successfully stored on the server or there were an error. Application can check this value
+ * using {@code getRecoveyStatus}.
+ *
+ * @param packageName Application whose recoverable key's status are to be updated.
+ * @param alias Application-specific key alias.
+ * @param status Status specific to recovery agent.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ public void setRecoveryStatus(
+ @NonNull String packageName, String alias, int status)
+ throws NameNotFoundException, InternalRecoveryServiceException {
+ try {
+ // TODO: update aidl
+ String[] aliases = alias == null ? null : new String[]{alias};
+ mBinder.setRecoveryStatus(packageName, aliases, status);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns recovery status for Application's KeyStore key.
+ * Negative status values are reserved for recovery agent specific codes. List of common codes:
+ *
+ * <ul>
+ * <li>{@link #RECOVERY_STATUS_SYNCED}
+ * <li>{@link #RECOVERY_STATUS_SYNC_IN_PROGRESS}
+ * <li>{@link #RECOVERY_STATUS_MISSING_ACCOUNT}
+ * <li>{@link #RECOVERY_STATUS_PERMANENT_FAILURE}
+ * </ul>
+ *
+ * @param packageName Application whose recoverable key status is returned.
+ * @param alias Application-specific key alias.
+ * @return Recovery status.
+ * @see #setRecoveryStatus
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public int getRecoveryStatus(String packageName, String alias)
+ throws InternalRecoveryServiceException {
+ try {
+ // TODO: update aidl
+ Map<String, Integer> allStatuses = mBinder.getRecoveryStatus(packageName);
+ Integer status = allStatuses.get(alias);
+ if (status == null) {
+ return RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE;
+ } else {
+ return status;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Specifies a set of secret types used for end-to-end keystore encryption. Knowing all of them
+ * is necessary to recover data.
+ *
+ * @param secretTypes {@link KeyChainProtectionParams#TYPE_LOCKSCREEN} or {@link
+ * KeyChainProtectionParams#TYPE_CUSTOM_PASSWORD}
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void setRecoverySecretTypes(
+ @NonNull @KeyChainProtectionParams.UserSecretType int[] secretTypes)
+ throws InternalRecoveryServiceException {
+ try {
+ mBinder.setRecoverySecretTypes(secretTypes);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Defines a set of secret types used for end-to-end keystore encryption. Knowing all of them is
+ * necessary to generate KeyChainSnapshot.
+ *
+ * @return list of recovery secret types
+ * @see KeyChainSnapshot
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public @NonNull @KeyChainProtectionParams.UserSecretType int[] getRecoverySecretTypes()
+ throws InternalRecoveryServiceException {
+ try {
+ return mBinder.getRecoverySecretTypes();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns a list of recovery secret types, necessary to create a pending recovery snapshot.
+ * When user enters a secret of a pending type {@link #recoverySecretAvailable} should be
+ * called.
+ *
+ * @return list of recovery secret types
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @NonNull
+ public @KeyChainProtectionParams.UserSecretType int[] getPendingRecoverySecretTypes()
+ throws InternalRecoveryServiceException {
+ try {
+ return mBinder.getPendingRecoverySecretTypes();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Method notifies KeyStore that a user-generated secret is available. This method generates a
+ * symmetric session key which a trusted remote device can use to return a recovery key. Caller
+ * should use {@link KeyChainProtectionParams#clearSecret} to override the secret value in
+ * memory.
+ *
+ * @param recoverySecret user generated secret together with parameters necessary to regenerate
+ * it on a new device.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void recoverySecretAvailable(@NonNull KeyChainProtectionParams recoverySecret)
+ throws InternalRecoveryServiceException {
+ try {
+ mBinder.recoverySecretAvailable(recoverySecret);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Generates a AES256/GCM/NoPADDING key called {@code alias} and loads it into the recoverable
+ * key store. Returns the raw material of the key.
+ *
+ * @param alias The key alias.
+ * @param account The account associated with the key
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ * @throws LockScreenRequiredException if the user has not set a lock screen. This is required
+ * to generate recoverable keys, as the snapshots are encrypted using a key derived from the
+ * lock screen.
+ */
+ public byte[] generateAndStoreKey(@NonNull String alias, byte[] account)
+ throws InternalRecoveryServiceException, LockScreenRequiredException {
+ try {
+ // TODO: add account
+ return mBinder.generateAndStoreKey(alias);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_INSECURE_USER) {
+ throw new LockScreenRequiredException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Removes a key called {@code alias} from the recoverable key store.
+ *
+ * @param alias The key alias.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void removeKey(@NonNull String alias) throws InternalRecoveryServiceException {
+ try {
+ mBinder.removeKey(alias);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ InternalRecoveryServiceException wrapUnexpectedServiceSpecificException(
+ ServiceSpecificException e) {
+ if (e.errorCode == ERROR_SERVICE_INTERNAL_ERROR) {
+ return new InternalRecoveryServiceException(e.getMessage());
+ }
+
+ // Should never happen. If it does, it's a bug, and we need to update how the method that
+ // called this throws its exceptions.
+ return new InternalRecoveryServiceException("Unexpected error code for method: "
+ + e.errorCode, e);
+ }
+}
diff --git a/android/security/keystore/recovery/RecoveryControllerException.java b/android/security/keystore/recovery/RecoveryControllerException.java
new file mode 100644
index 0000000..2733aca
--- /dev/null
+++ b/android/security/keystore/recovery/RecoveryControllerException.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 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.security.keystore.recovery;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * Base exception for errors thrown by {@link RecoveryController}.
+ *
+ * @hide
+ * Deprecated
+ */
+public abstract class RecoveryControllerException extends GeneralSecurityException {
+ RecoveryControllerException() { }
+
+ RecoveryControllerException(String msg) {
+ super(msg);
+ }
+
+ public RecoveryControllerException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/android/security/keystore/recovery/RecoverySession.java b/android/security/keystore/recovery/RecoverySession.java
new file mode 100644
index 0000000..4db5d6e
--- /dev/null
+++ b/android/security/keystore/recovery/RecoverySession.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2018 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.security.keystore.recovery;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Session to recover a {@link KeyChainSnapshot} from the remote trusted hardware, initiated by a
+ * recovery agent.
+ *
+ * @hide
+ */
+@SystemApi
+public class RecoverySession implements AutoCloseable {
+ private static final String TAG = "RecoverySession";
+
+ private static final int SESSION_ID_LENGTH_BYTES = 16;
+
+ private final String mSessionId;
+ private final RecoveryController mRecoveryController;
+
+ private RecoverySession(RecoveryController recoveryController, String sessionId) {
+ mRecoveryController = recoveryController;
+ mSessionId = sessionId;
+ }
+
+ /**
+ * A new session, started by {@code recoveryManager}.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ static RecoverySession newInstance(RecoveryController recoveryController) {
+ return new RecoverySession(recoveryController, newSessionId());
+ }
+
+ /**
+ * Returns a new random session ID.
+ */
+ private static String newSessionId() {
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES];
+ secureRandom.nextBytes(sessionId);
+ StringBuilder sb = new StringBuilder();
+ for (byte b : sessionId) {
+ sb.append(Byte.toHexString(b, /*upperCase=*/ false));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Starts a recovery session and returns a blob with proof of recovery secret possession.
+ * The method generates a symmetric key for a session, which trusted remote device can use to
+ * return recovery key.
+ *
+ * @param verifierPublicKey Encoded {@code java.security.cert.X509Certificate} with Public key
+ * used to create the recovery blob on the source device.
+ * Keystore will verify the certificate using root of trust.
+ * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
+ * Used to limit number of guesses.
+ * @param vaultChallenge Data passed from server for this recovery session and used to prevent
+ * replay attacks
+ * @param secrets Secrets provided by user, the method only uses type and secret fields.
+ * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is
+ * encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric
+ * key and parameters necessary to identify the counter with the number of failed recovery
+ * attempts.
+ * @throws CertificateException if the {@code verifierPublicKey} is in an incorrect
+ * format.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ @NonNull public byte[] start(
+ @NonNull byte[] verifierPublicKey,
+ @NonNull byte[] vaultParams,
+ @NonNull byte[] vaultChallenge,
+ @NonNull List<KeyChainProtectionParams> secrets)
+ throws CertificateException, InternalRecoveryServiceException {
+ try {
+ byte[] recoveryClaim =
+ mRecoveryController.getBinder().startRecoverySession(
+ mSessionId,
+ verifierPublicKey,
+ vaultParams,
+ vaultChallenge,
+ secrets);
+ return recoveryClaim;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT) {
+ throw new CertificateException(e.getMessage());
+ }
+ throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Imports keys.
+ *
+ * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
+ * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
+ * and session. KeyStore only uses package names from the application info in {@link
+ * WrappedApplicationKey}. Caller is responsibility to perform certificates check.
+ * @return Map from alias to raw key material.
+ * @throws SessionExpiredException if {@code session} has since been closed.
+ * @throws DecryptionFailedException if unable to decrypt the snapshot.
+ * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ public Map<String, byte[]> recoverKeys(
+ @NonNull byte[] recoveryKeyBlob,
+ @NonNull List<WrappedApplicationKey> applicationKeys)
+ throws SessionExpiredException, DecryptionFailedException,
+ InternalRecoveryServiceException {
+ try {
+ return (Map<String, byte[]>) mRecoveryController.getBinder().recoverKeys(
+ mSessionId, recoveryKeyBlob, applicationKeys);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == RecoveryController.ERROR_DECRYPTION_FAILED) {
+ throw new DecryptionFailedException(e.getMessage());
+ }
+ if (e.errorCode == RecoveryController.ERROR_SESSION_EXPIRED) {
+ throw new SessionExpiredException(e.getMessage());
+ }
+ throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * An internal session ID, used by the framework to match recovery claims to snapshot responses.
+ *
+ * @hide
+ */
+ String getSessionId() {
+ return mSessionId;
+ }
+
+ /**
+ * Deletes all data associated with {@code session}. Should not be invoked directly but via
+ * {@link RecoverySession#close()}.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ @Override
+ public void close() {
+ try {
+ mRecoveryController.getBinder().closeSession(mSessionId);
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Unexpected error trying to close session", e);
+ }
+ }
+}
diff --git a/android/security/keystore/recovery/SessionExpiredException.java b/android/security/keystore/recovery/SessionExpiredException.java
new file mode 100644
index 0000000..8c18e41
--- /dev/null
+++ b/android/security/keystore/recovery/SessionExpiredException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 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.security.keystore.recovery;
+
+import android.annotation.SystemApi;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * Error thrown when attempting to use a {@link RecoverySession} that has since expired.
+ *
+ * @hide
+ */
+@SystemApi
+public class SessionExpiredException extends GeneralSecurityException {
+ public SessionExpiredException(String msg) {
+ super(msg);
+ }
+}
diff --git a/android/security/keystore/recovery/WrappedApplicationKey.java b/android/security/keystore/recovery/WrappedApplicationKey.java
new file mode 100644
index 0000000..f360bbe
--- /dev/null
+++ b/android/security/keystore/recovery/WrappedApplicationKey.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2017 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.security.keystore.recovery;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Helper class with data necessary recover a single application key, given a recovery key.
+ *
+ * <ul>
+ * <li>Alias - Keystore alias of the key.
+ * <li>Account Recovery Agent specific account associated with the key.
+ * <li>Encrypted key material.
+ * </ul>
+ *
+ * Note that Application info is not included. Recovery Agent can only make its own keys
+ * recoverable.
+ *
+ * @hide
+ */
+@SystemApi
+public final class WrappedApplicationKey implements Parcelable {
+ private String mAlias;
+ // The only supported format is AES-256 symmetric key.
+ private byte[] mEncryptedKeyMaterial;
+ private byte[] mAccount;
+
+ /**
+ * Builder for creating {@link WrappedApplicationKey}.
+ */
+ public static class Builder {
+ private WrappedApplicationKey mInstance = new WrappedApplicationKey();
+
+ /**
+ * Sets Application-specific alias of the key.
+ *
+ * @param alias The alias.
+ * @return This builder.
+ */
+ public Builder setAlias(@NonNull String alias) {
+ mInstance.mAlias = alias;
+ return this;
+ }
+
+ /**
+ * Sets Recovery agent specific account.
+ *
+ * @param account The account.
+ * @return This builder.
+ */
+ public Builder setAccount(@NonNull byte[] account) {
+ mInstance.mAccount = account;
+ return this;
+ }
+
+ /**
+ * Sets key material encrypted by recovery key.
+ *
+ * @param encryptedKeyMaterial The key material
+ * @return This builder
+ */
+
+ public Builder setEncryptedKeyMaterial(@NonNull byte[] encryptedKeyMaterial) {
+ mInstance.mEncryptedKeyMaterial = encryptedKeyMaterial;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link WrappedApplicationKey} instance.
+ *
+ * @return new instance
+ * @throws NullPointerException if some required fields were not set.
+ */
+ @NonNull public WrappedApplicationKey build() {
+ Preconditions.checkNotNull(mInstance.mAlias);
+ Preconditions.checkNotNull(mInstance.mEncryptedKeyMaterial);
+ if (mInstance.mAccount == null) {
+ mInstance.mAccount = new byte[]{};
+ }
+ return mInstance;
+ }
+ }
+
+ private WrappedApplicationKey() {
+ }
+
+ /**
+ * Deprecated - consider using Builder.
+ * @hide
+ */
+ public WrappedApplicationKey(@NonNull String alias, @NonNull byte[] encryptedKeyMaterial) {
+ mAlias = Preconditions.checkNotNull(alias);
+ mEncryptedKeyMaterial = Preconditions.checkNotNull(encryptedKeyMaterial);
+ }
+
+ /**
+ * Application-specific alias of the key.
+ *
+ * @see java.security.KeyStore.aliases
+ */
+ public @NonNull String getAlias() {
+ return mAlias;
+ }
+
+ /** Key material encrypted by recovery key. */
+ public @NonNull byte[] getEncryptedKeyMaterial() {
+ return mEncryptedKeyMaterial;
+ }
+
+ /** Account, default value is empty array */
+ public @NonNull byte[] getAccount() {
+ if (mAccount == null) {
+ return new byte[]{};
+ }
+ return mAccount;
+ }
+
+ public static final Parcelable.Creator<WrappedApplicationKey> CREATOR =
+ new Parcelable.Creator<WrappedApplicationKey>() {
+ public WrappedApplicationKey createFromParcel(Parcel in) {
+ return new WrappedApplicationKey(in);
+ }
+
+ public WrappedApplicationKey[] newArray(int length) {
+ return new WrappedApplicationKey[length];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mAlias);
+ out.writeByteArray(mEncryptedKeyMaterial);
+ out.writeByteArray(mAccount);
+ }
+
+ /**
+ * @hide
+ */
+ protected WrappedApplicationKey(Parcel in) {
+ mAlias = in.readString();
+ mEncryptedKeyMaterial = in.createByteArray();
+ mAccount = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/security/recoverablekeystore/KeyEntryRecoveryData.java b/android/security/recoverablekeystore/KeyEntryRecoveryData.java
deleted file mode 100644
index 80f5aa7..0000000
--- a/android/security/recoverablekeystore/KeyEntryRecoveryData.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2017 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.security.recoverablekeystore;
-
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.internal.util.Preconditions;
-
-
-/**
- * Helper class with data necessary recover a single application key, given a recovery key.
- *
- * <ul>
- * <li>Alias - Keystore alias of the key.
- * <li>Encrypted key material.
- * </ul>
- *
- * Note that Application info is not included. Recovery Agent can only make its own keys
- * recoverable.
- *
- * @hide
- */
-public final class KeyEntryRecoveryData implements Parcelable {
- private final byte[] mAlias;
- // The only supported format is AES-256 symmetric key.
- private final byte[] mEncryptedKeyMaterial;
-
- public KeyEntryRecoveryData(@NonNull byte[] alias, @NonNull byte[] encryptedKeyMaterial) {
- mAlias = Preconditions.checkNotNull(alias);
- mEncryptedKeyMaterial = Preconditions.checkNotNull(encryptedKeyMaterial);
- }
-
- /**
- * Application-specific alias of the key.
- * @see java.security.KeyStore.aliases
- */
- public @NonNull byte[] getAlias() {
- return mAlias;
- }
-
- /**
- * Encrypted key material encrypted by recovery key.
- */
- public @NonNull byte[] getEncryptedKeyMaterial() {
- return mEncryptedKeyMaterial;
- }
-
- public static final Parcelable.Creator<KeyEntryRecoveryData> CREATOR =
- new Parcelable.Creator<KeyEntryRecoveryData>() {
- public KeyEntryRecoveryData createFromParcel(Parcel in) {
- return new KeyEntryRecoveryData(in);
- }
-
- public KeyEntryRecoveryData[] newArray(int length) {
- return new KeyEntryRecoveryData[length];
- }
- };
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- out.writeByteArray(mAlias);
- out.writeByteArray(mEncryptedKeyMaterial);
- }
-
- protected KeyEntryRecoveryData(Parcel in) {
- mAlias = in.createByteArray();
- mEncryptedKeyMaterial = in.createByteArray();
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-}
diff --git a/android/security/recoverablekeystore/KeyStoreRecoveryData.java b/android/security/recoverablekeystore/KeyStoreRecoveryData.java
deleted file mode 100644
index 087f7a2..0000000
--- a/android/security/recoverablekeystore/KeyStoreRecoveryData.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2017 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.security.recoverablekeystore;
-
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.internal.util.Preconditions;
-
-import java.util.List;
-
-/**
- * Helper class which returns data necessary to recover keys.
- * Contains
- *
- * <ul>
- * <li>Snapshot version.
- * <li>Recovery metadata with UI and key derivation parameters.
- * <li>List of application keys encrypted by recovery key.
- * <li>Encrypted recovery key.
- * </ul>
- *
- * @hide
- */
-public final class KeyStoreRecoveryData implements Parcelable {
- private final int mSnapshotVersion;
- private final List<KeyStoreRecoveryMetadata> mRecoveryMetadata;
- private final List<KeyEntryRecoveryData> mApplicationKeyBlobs;
- private final byte[] mEncryptedRecoveryKeyBlob;
-
- public KeyStoreRecoveryData(int snapshotVersion, @NonNull List<KeyStoreRecoveryMetadata>
- recoveryMetadata, @NonNull List<KeyEntryRecoveryData> applicationKeyBlobs,
- @NonNull byte[] encryptedRecoveryKeyBlob) {
- mSnapshotVersion = snapshotVersion;
- mRecoveryMetadata = Preconditions.checkNotNull(recoveryMetadata);
- mApplicationKeyBlobs = Preconditions.checkNotNull(applicationKeyBlobs);
- mEncryptedRecoveryKeyBlob = Preconditions.checkNotNull(encryptedRecoveryKeyBlob);
- }
-
- /**
- * Snapshot version for given account. It is incremented when user secret or list of application
- * keys changes.
- */
- public int getSnapshotVersion() {
- return mSnapshotVersion;
- }
-
- /**
- * UI and key derivation parameters. Note that combination of secrets may be used.
- */
- public @NonNull List<KeyStoreRecoveryMetadata> getRecoveryMetadata() {
- return mRecoveryMetadata;
- }
-
- /**
- * List of application keys, with key material encrypted by
- * the recovery key ({@link #getEncryptedRecoveryKeyBlob}).
- */
- public @NonNull List<KeyEntryRecoveryData> getApplicationKeyBlobs() {
- return mApplicationKeyBlobs;
- }
-
- /**
- * Recovery key blob, encrypted by user secret and recovery service public key.
- */
- public @NonNull byte[] getEncryptedRecoveryKeyBlob() {
- return mEncryptedRecoveryKeyBlob;
- }
-
- public static final Parcelable.Creator<KeyStoreRecoveryData> CREATOR =
- new Parcelable.Creator<KeyStoreRecoveryData>() {
- public KeyStoreRecoveryData createFromParcel(Parcel in) {
- return new KeyStoreRecoveryData(in);
- }
-
- public KeyStoreRecoveryData[] newArray(int length) {
- return new KeyStoreRecoveryData[length];
- }
- };
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- out.writeInt(mSnapshotVersion);
- out.writeTypedList(mRecoveryMetadata);
- out.writeByteArray(mEncryptedRecoveryKeyBlob);
- out.writeTypedList(mApplicationKeyBlobs);
- }
-
- protected KeyStoreRecoveryData(Parcel in) {
- mSnapshotVersion = in.readInt();
- mRecoveryMetadata = in.createTypedArrayList(KeyStoreRecoveryMetadata.CREATOR);
- mEncryptedRecoveryKeyBlob = in.createByteArray();
- mApplicationKeyBlobs = in.createTypedArrayList(KeyEntryRecoveryData.CREATOR);
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-}
diff --git a/android/security/recoverablekeystore/KeyStoreRecoveryMetadata.java b/android/security/recoverablekeystore/KeyStoreRecoveryMetadata.java
deleted file mode 100644
index 43f9c80..0000000
--- a/android/security/recoverablekeystore/KeyStoreRecoveryMetadata.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright (C) 2017 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.security.recoverablekeystore;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.internal.util.Preconditions;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.Arrays;
-
-/**
- * Helper class with data necessary to recover Keystore on a new device.
- * It defines UI shown to the user and a way to derive a cryptographic key from user output.
- *
- * @hide
- */
-public final class KeyStoreRecoveryMetadata implements Parcelable {
- /** @hide */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({TYPE_LOCKSCREEN, TYPE_CUSTOM_PASSWORD})
- public @interface UserSecretType {
- }
-
- /**
- * Lockscreen secret is required to recover KeyStore.
- */
- public static final int TYPE_LOCKSCREEN = 1;
-
- /**
- * Custom passphrase, unrelated to lock screen, is required to recover KeyStore.
- */
- public static final int TYPE_CUSTOM_PASSWORD = 2;
-
- /** @hide */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({TYPE_PIN, TYPE_PASSWORD, TYPE_PATTERN})
- public @interface LockScreenUiFormat {
- }
-
- /**
- * Pin with digits only.
- */
- public static final int TYPE_PIN = 1;
-
- /**
- * Password. String with latin-1 characters only.
- */
- public static final int TYPE_PASSWORD = 2;
-
- /**
- * Pattern with 3 by 3 grid.
- */
- public static final int TYPE_PATTERN = 3;
-
- @UserSecretType
- private final int mUserSecretType;
-
- @LockScreenUiFormat
- private final int mLockScreenUiFormat;
-
- /**
- * Parameters of key derivation function, including algorithm, difficulty, salt.
- */
- private KeyDerivationParameters mKeyDerivationParameters;
- private byte[] mSecret; // Derived from user secret. The field must have limited visibility.
-
- /**
- * @param secret Constructor creates a reference to the secret. Caller must use
- * @link {#clearSecret} to overwrite its value in memory.
- */
- public KeyStoreRecoveryMetadata(@UserSecretType int userSecretType,
- @LockScreenUiFormat int lockScreenUiFormat,
- @NonNull KeyDerivationParameters keyDerivationParameters, @NonNull byte[] secret) {
- mUserSecretType = userSecretType;
- mLockScreenUiFormat = lockScreenUiFormat;
- mKeyDerivationParameters = Preconditions.checkNotNull(keyDerivationParameters);
- mSecret = Preconditions.checkNotNull(secret);
- }
-
- /**
- * Specifies UX shown to user during recovery.
- *
- * @see KeyStore.TYPE_PIN
- * @see KeyStore.TYPE_PASSWORD
- * @see KeyStore.TYPE_PATTERN
- */
- public @LockScreenUiFormat int getLockScreenUiFormat() {
- return mLockScreenUiFormat;
- }
-
- /**
- * Specifies function used to derive symmetric key from user input
- * Format is defined in separate util class.
- */
- public @NonNull KeyDerivationParameters getKeyDerivationParameters() {
- return mKeyDerivationParameters;
- }
-
- /**
- * Secret string derived from user input.
- */
- public @NonNull byte[] getSecret() {
- return mSecret;
- }
-
- /**
- * @see KeyStore.TYPE_LOCKSCREEN
- * @see KeyStore.TYPE_CUSTOM_PASSWORD
- */
- public @UserSecretType int getUserSecretType() {
- return mUserSecretType;
- }
-
- /**
- * Removes secret from memory than object is no longer used.
- * Since finalizer call is not reliable, please use @link {#clearSecret} directly.
- */
- @Override
- protected void finalize() throws Throwable {
- clearSecret();
- super.finalize();
- }
-
- /**
- * Fills mSecret with zeroes.
- */
- public void clearSecret() {
- Arrays.fill(mSecret, (byte) 0);
- }
-
- public static final Parcelable.Creator<KeyStoreRecoveryMetadata> CREATOR =
- new Parcelable.Creator<KeyStoreRecoveryMetadata>() {
- public KeyStoreRecoveryMetadata createFromParcel(Parcel in) {
- return new KeyStoreRecoveryMetadata(in);
- }
-
- public KeyStoreRecoveryMetadata[] newArray(int length) {
- return new KeyStoreRecoveryMetadata[length];
- }
- };
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- out.writeInt(mUserSecretType);
- out.writeInt(mLockScreenUiFormat);
- out.writeTypedObject(mKeyDerivationParameters, flags);
- out.writeByteArray(mSecret);
- }
-
- protected KeyStoreRecoveryMetadata(Parcel in) {
- mUserSecretType = in.readInt();
- mLockScreenUiFormat = in.readInt();
- mKeyDerivationParameters = in.readTypedObject(KeyDerivationParameters.CREATOR);
- mSecret = in.createByteArray();
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-}
diff --git a/android/security/recoverablekeystore/RecoverableKeyStoreLoader.java b/android/security/recoverablekeystore/RecoverableKeyStoreLoader.java
deleted file mode 100644
index 72a138a..0000000
--- a/android/security/recoverablekeystore/RecoverableKeyStoreLoader.java
+++ /dev/null
@@ -1,467 +0,0 @@
-/*
- * Copyright (C) 2017 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.security.recoverablekeystore;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.PendingIntent;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.os.ServiceSpecificException;
-import android.os.UserHandle;
-import android.security.KeyStore;
-import android.util.AndroidException;
-
-import com.android.internal.widget.ILockSettings;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * A wrapper around KeyStore which lets key be exported to trusted hardware on server side and
- * recovered later.
- *
- * @hide
- */
-public class RecoverableKeyStoreLoader {
-
- public static final String PERMISSION_RECOVER_KEYSTORE = "android.permission.RECOVER_KEYSTORE";
-
- public static final int NO_ERROR = KeyStore.NO_ERROR;
- public static final int SYSTEM_ERROR = KeyStore.SYSTEM_ERROR;
- public static final int UNINITIALIZED_RECOVERY_PUBLIC_KEY = 20;
- public static final int NO_SNAPSHOT_PENDING_ERROR = 21;
-
- /**
- * Rate limit is enforced to prevent using too many trusted remote devices, since each device
- * can have its own number of user secret guesses allowed.
- *
- * @hide
- */
- public static final int RATE_LIMIT_EXCEEDED = 21;
-
- /** Key has been successfully synced. */
- public static final int RECOVERY_STATUS_SYNCED = 0;
- /** Waiting for recovery agent to sync the key. */
- public static final int RECOVERY_STATUS_SYNC_IN_PROGRESS = 1;
- /** Recovery account is not available. */
- public static final int RECOVERY_STATUS_MISSING_ACCOUNT = 2;
- /** Key cannot be synced. */
- public static final int RECOVERY_STATUS_PERMANENT_FAILURE = 3;
-
- private final ILockSettings mBinder;
-
- private RecoverableKeyStoreLoader(ILockSettings binder) {
- mBinder = binder;
- }
-
- /** @hide */
- public static RecoverableKeyStoreLoader getInstance() {
- ILockSettings lockSettings =
- ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings"));
- return new RecoverableKeyStoreLoader(lockSettings);
- }
-
- /**
- * Exceptions returned by {@link RecoverableKeyStoreLoader}.
- *
- * @hide
- */
- public static class RecoverableKeyStoreLoaderException extends AndroidException {
- private int mErrorCode;
-
- /**
- * Creates new {@link #RecoverableKeyStoreLoaderException} instance from the error code.
- *
- * @param errorCode
- * @hide
- */
- public static RecoverableKeyStoreLoaderException fromErrorCode(int errorCode) {
- return new RecoverableKeyStoreLoaderException(
- errorCode, getMessageFromErrorCode(errorCode));
- }
-
- /**
- * Creates new {@link #RecoverableKeyStoreLoaderException} from {@link
- * ServiceSpecificException}.
- *
- * @param e exception thrown on service side.
- * @hide
- */
- static RecoverableKeyStoreLoaderException fromServiceSpecificException(
- ServiceSpecificException e) throws RecoverableKeyStoreLoaderException {
- throw RecoverableKeyStoreLoaderException.fromErrorCode(e.errorCode);
- }
-
- private RecoverableKeyStoreLoaderException(int errorCode, String message) {
- super(message);
- }
-
- /** Returns errorCode. */
- public int getErrorCode() {
- return mErrorCode;
- }
-
- /** @hide */
- private static String getMessageFromErrorCode(int errorCode) {
- switch (errorCode) {
- case NO_ERROR:
- return "OK";
- case SYSTEM_ERROR:
- return "System error";
- case UNINITIALIZED_RECOVERY_PUBLIC_KEY:
- return "Recovery service is not initialized";
- case RATE_LIMIT_EXCEEDED:
- return "Rate limit exceeded";
- default:
- return String.valueOf("Unknown error code " + errorCode);
- }
- }
- }
-
- /**
- * Initializes key recovery service for the calling application. RecoverableKeyStoreLoader
- * randomly chooses one of the keys from the list and keeps it to use for future key export
- * operations. Collection of all keys in the list must be signed by the provided {@code
- * rootCertificateAlias}, which must also be present in the list of root certificates
- * preinstalled on the device. The random selection allows RecoverableKeyStoreLoader to select
- * which of a set of remote recovery service devices will be used.
- *
- * <p>In addition, RecoverableKeyStoreLoader enforces a delay of three months between
- * consecutive initialization attempts, to limit the ability of an attacker to often switch
- * remote recovery devices and significantly increase number of recovery attempts.
- *
- * @param rootCertificateAlias alias of a root certificate preinstalled on the device
- * @param signedPublicKeyList binary blob a list of X509 certificates and signature
- * @throws RecoverableKeyStoreLoaderException if signature is invalid, or key rotation was rate
- * limited.
- * @hide
- */
- public void initRecoveryService(
- @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList)
- throws RecoverableKeyStoreLoaderException {
- try {
- mBinder.initRecoveryService(
- rootCertificateAlias, signedPublicKeyList, UserHandle.getCallingUserId());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
- }
- }
-
- /**
- * Returns data necessary to store all recoverable keys for given account. Key material is
- * encrypted with user secret and recovery public key.
- *
- * @param account specific to Recovery agent.
- * @return Data necessary to recover keystore.
- * @hide
- */
- public @NonNull KeyStoreRecoveryData getRecoveryData(@NonNull byte[] account)
- throws RecoverableKeyStoreLoaderException {
- try {
- KeyStoreRecoveryData recoveryData =
- mBinder.getRecoveryData(account, UserHandle.getCallingUserId());
- return recoveryData;
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
- }
- }
-
- /**
- * Sets a listener which notifies recovery agent that new recovery snapshot is available. {@link
- * #getRecoveryData} can be used to get the snapshot. Note that every recovery agent can have at
- * most one registered listener at any time.
- *
- * @param intent triggered when new snapshot is available. Unregisters listener if the value is
- * {@code null}.
- * @hide
- */
- public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent)
- throws RecoverableKeyStoreLoaderException {
- try {
- mBinder.setSnapshotCreatedPendingIntent(intent, UserHandle.getCallingUserId());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
- }
- }
-
- /**
- * Returns a map from recovery agent accounts to corresponding KeyStore recovery snapshot
- * version. Version zero is used, if no snapshots were created for the account.
- *
- * @return Map from recovery agent accounts to snapshot versions.
- * @see KeyStoreRecoveryData#getSnapshotVersion
- * @hide
- */
- public @NonNull Map<byte[], Integer> getRecoverySnapshotVersions()
- throws RecoverableKeyStoreLoaderException {
- try {
- // IPC doesn't support generic Maps.
- @SuppressWarnings("unchecked")
- Map<byte[], Integer> result =
- (Map<byte[], Integer>)
- mBinder.getRecoverySnapshotVersions(UserHandle.getCallingUserId());
- return result;
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
- }
- }
-
- /**
- * Server parameters used to generate new recovery key blobs. This value will be included in
- * {@code KeyStoreRecoveryData.getEncryptedRecoveryKeyBlob()}. The same value must be included
- * in vaultParams {@link #startRecoverySession}
- *
- * @param serverParameters included in recovery key blob.
- * @see #getRecoveryData
- * @throws RecoverableKeyStoreLoaderException If parameters rotation is rate limited.
- * @hide
- */
- public void setServerParameters(long serverParameters)
- throws RecoverableKeyStoreLoaderException {
- try {
- mBinder.setServerParameters(serverParameters, UserHandle.getCallingUserId());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
- }
- }
-
- /**
- * Updates recovery status for given keys. It is used to notify keystore that key was
- * successfully stored on the server or there were an error. Application can check this value
- * using {@code getRecoveyStatus}.
- *
- * @param packageName Application whose recoverable keys' statuses are to be updated.
- * @param aliases List of application-specific key aliases. If the array is empty, updates the
- * status for all existing recoverable keys.
- * @param status Status specific to recovery agent.
- */
- public void setRecoveryStatus(
- @NonNull String packageName, @Nullable String[] aliases, int status)
- throws NameNotFoundException, RecoverableKeyStoreLoaderException {
- try {
- mBinder.setRecoveryStatus(packageName, aliases, status, UserHandle.getCallingUserId());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
- }
- }
-
- /**
- * Returns a {@code Map} from Application's KeyStore key aliases to their recovery status.
- * Negative status values are reserved for recovery agent specific codes. List of common codes:
- *
- * <ul>
- * <li>{@link #RECOVERY_STATUS_SYNCED}
- * <li>{@link #RECOVERY_STATUS_SYNC_IN_PROGRESS}
- * <li>{@link #RECOVERY_STATUS_MISSING_ACCOUNT}
- * <li>{@link #RECOVERY_STATUS_PERMANENT_FAILURE}
- * </ul>
- *
- * @param packageName Application whose recoverable keys' statuses are to be retrieved. if
- * {@code null} caller's package will be used.
- * @return {@code Map} from KeyStore alias to recovery status.
- * @see #setRecoveryStatus
- * @hide
- */
- public Map<String, Integer> getRecoveryStatus(@Nullable String packageName)
- throws RecoverableKeyStoreLoaderException {
- try {
- // IPC doesn't support generic Maps.
- @SuppressWarnings("unchecked")
- Map<String, Integer> result =
- (Map<String, Integer>)
- mBinder.getRecoveryStatus(packageName, UserHandle.getCallingUserId());
- return result;
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
- }
- }
-
- /**
- * Specifies a set of secret types used for end-to-end keystore encryption. Knowing all of them
- * is necessary to recover data.
- *
- * @param secretTypes {@link KeyStoreRecoveryMetadata#TYPE_LOCKSCREEN} or {@link
- * KeyStoreRecoveryMetadata#TYPE_CUSTOM_PASSWORD}
- */
- public void setRecoverySecretTypes(
- @NonNull @KeyStoreRecoveryMetadata.UserSecretType int[] secretTypes)
- throws RecoverableKeyStoreLoaderException {
- try {
- mBinder.setRecoverySecretTypes(secretTypes, UserHandle.getCallingUserId());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
- }
- }
-
- /**
- * Defines a set of secret types used for end-to-end keystore encryption. Knowing all of them is
- * necessary to generate KeyStoreRecoveryData.
- *
- * @return list of recovery secret types
- * @see KeyStoreRecoveryData
- */
- public @NonNull @KeyStoreRecoveryMetadata.UserSecretType int[] getRecoverySecretTypes()
- throws RecoverableKeyStoreLoaderException {
- try {
- return mBinder.getRecoverySecretTypes(UserHandle.getCallingUserId());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
- }
- }
-
- /**
- * Returns a list of recovery secret types, necessary to create a pending recovery snapshot.
- * When user enters a secret of a pending type {@link #recoverySecretAvailable} should be
- * called.
- *
- * @return list of recovery secret types
- */
- public @NonNull @KeyStoreRecoveryMetadata.UserSecretType int[] getPendingRecoverySecretTypes()
- throws RecoverableKeyStoreLoaderException {
- try {
- return mBinder.getPendingRecoverySecretTypes(UserHandle.getCallingUserId());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
- }
- }
-
- /**
- * Method notifies KeyStore that a user-generated secret is available. This method generates a
- * symmetric session key which a trusted remote device can use to return a recovery key. Caller
- * should use {@link KeyStoreRecoveryMetadata#clearSecret} to override the secret value in
- * memory.
- *
- * @param recoverySecret user generated secret together with parameters necessary to regenerate
- * it on a new device.
- */
- public void recoverySecretAvailable(@NonNull KeyStoreRecoveryMetadata recoverySecret)
- throws RecoverableKeyStoreLoaderException {
- try {
- mBinder.recoverySecretAvailable(recoverySecret, UserHandle.getCallingUserId());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
- }
- }
-
- /**
- * Initializes recovery session and returns a blob with proof of recovery secrets possession.
- * The method generates symmetric key for a session, which trusted remote device can use to
- * return recovery key.
- *
- * @param sessionId ID for recovery session.
- * @param verifierPublicKey Certificate with Public key used to create the recovery blob on the
- * source device. Keystore will verify the certificate using root of trust.
- * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
- * Used to limit number of guesses.
- * @param vaultChallenge Data passed from server for this recovery session and used to prevent
- * replay attacks
- * @param secrets Secrets provided by user, the method only uses type and secret fields.
- * @return Binary blob with recovery claim. It is encrypted with verifierPublicKey and contains
- * a proof of user secrets, session symmetric key and parameters necessary to identify the
- * counter with the number of failed recovery attempts.
- */
- public @NonNull byte[] startRecoverySession(
- @NonNull String sessionId,
- @NonNull byte[] verifierPublicKey,
- @NonNull byte[] vaultParams,
- @NonNull byte[] vaultChallenge,
- @NonNull List<KeyStoreRecoveryMetadata> secrets)
- throws RecoverableKeyStoreLoaderException {
- try {
- byte[] recoveryClaim =
- mBinder.startRecoverySession(
- sessionId,
- verifierPublicKey,
- vaultParams,
- vaultChallenge,
- secrets,
- UserHandle.getCallingUserId());
- return recoveryClaim;
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
- }
- }
-
- /**
- * Imports keys.
- *
- * @param sessionId Id for recovery session, same as in
- * {@link #startRecoverySession(String, byte[], byte[], byte[], List)} on}.
- * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
- * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
- * and session. KeyStore only uses package names from the application info in {@link
- * KeyEntryRecoveryData}. Caller is responsibility to perform certificates check.
- * @return Map from alias to raw key material.
- */
- public Map<String, byte[]> recoverKeys(
- @NonNull String sessionId,
- @NonNull byte[] recoveryKeyBlob,
- @NonNull List<KeyEntryRecoveryData> applicationKeys)
- throws RecoverableKeyStoreLoaderException {
- try {
- return (Map<String, byte[]>) mBinder.recoverKeys(
- sessionId, recoveryKeyBlob, applicationKeys, UserHandle.getCallingUserId());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
- }
- }
-
- /**
- * Generates a key called {@code alias} and loads it into the recoverable key store. Returns the
- * raw material of the key.
- *
- * @throws RecoverableKeyStoreLoaderException if an error occurred generating and storing the
- * key.
- */
- public byte[] generateAndStoreKey(String alias) throws RecoverableKeyStoreLoaderException {
- try {
- return mBinder.generateAndStoreKey(alias);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
- }
- }
-}
diff --git a/android/service/autofill/AutofillFieldClassificationService.java b/android/service/autofill/AutofillFieldClassificationService.java
new file mode 100644
index 0000000..1ef6100
--- /dev/null
+++ b/android/service/autofill/AutofillFieldClassificationService.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2018 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.service.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.os.HandlerCaller;
+import com.android.internal.os.SomeArgs;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A service that calculates field classification scores.
+ *
+ * <p>A field classification score is a {@code float} representing how well an
+ * {@link AutofillValue} filled matches a expected value predicted by an autofill service
+ * —a full-match is {@code 1.0} (representing 100%), while a full mismatch is {@code 0.0}.
+ *
+ * <p>The exact score depends on the algorithm used to calculate it— the service must provide
+ * at least one default algorithm (which is used when the algorithm is not specified or is invalid),
+ * but it could provide more (in which case the algorithm name should be specifiied by the caller
+ * when calculating the scores).
+ *
+ * {@hide}
+ */
+@SystemApi
+public abstract class AutofillFieldClassificationService extends Service {
+
+ private static final String TAG = "AutofillFieldClassificationService";
+
+ private static final int MSG_GET_SCORES = 1;
+
+ /**
+ * The {@link Intent} action that must be declared as handled by a service
+ * in its manifest for the system to recognize it as a quota providing service.
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.service.autofill.AutofillFieldClassificationService";
+
+ /**
+ * Manifest metadata key for the resource string containing the name of the default field
+ * classification algorithm.
+ */
+ public static final String SERVICE_META_DATA_KEY_DEFAULT_ALGORITHM =
+ "android.autofill.field_classification.default_algorithm";
+ /**
+ * Manifest metadata key for the resource string array containing the names of all field
+ * classification algorithms provided by the service.
+ */
+ public static final String SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS =
+ "android.autofill.field_classification.available_algorithms";
+
+
+ /** {@hide} **/
+ public static final String EXTRA_SCORES = "scores";
+
+ private AutofillFieldClassificationServiceWrapper mWrapper;
+
+ private final HandlerCaller.Callback mHandlerCallback = (msg) -> {
+ final int action = msg.what;
+ final Bundle data = new Bundle();
+ final RemoteCallback callback;
+ switch (action) {
+ case MSG_GET_SCORES:
+ final SomeArgs args = (SomeArgs) msg.obj;
+ callback = (RemoteCallback) args.arg1;
+ final String algorithmName = (String) args.arg2;
+ final Bundle algorithmArgs = (Bundle) args.arg3;
+ @SuppressWarnings("unchecked")
+ final List<AutofillValue> actualValues = ((List<AutofillValue>) args.arg4);
+ @SuppressWarnings("unchecked")
+ final String[] userDataValues = (String[]) args.arg5;
+ final float[][] scores = onGetScores(algorithmName, algorithmArgs, actualValues,
+ Arrays.asList(userDataValues));
+ if (scores != null) {
+ data.putParcelable(EXTRA_SCORES, new Scores(scores));
+ }
+ break;
+ default:
+ Log.w(TAG, "Handling unknown message: " + action);
+ return;
+ }
+ callback.sendResult(data);
+ };
+
+ private final HandlerCaller mHandlerCaller = new HandlerCaller(null, Looper.getMainLooper(),
+ mHandlerCallback, true);
+
+ /** @hide */
+ public AutofillFieldClassificationService() {
+
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mWrapper = new AutofillFieldClassificationServiceWrapper();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mWrapper;
+ }
+
+ /**
+ * Calculates field classification scores in a batch.
+ *
+ * <p>See {@link AutofillFieldClassificationService} for more info about field classification
+ * scores.
+ *
+ * @param algorithm name of the algorithm to be used to calculate the scores. If invalid, the
+ * default algorithm will be used instead.
+ * @param args optional arguments to be passed to the algorithm.
+ * @param actualValues values entered by the user.
+ * @param userDataValues values predicted from the user data.
+ * @return the calculated scores, with the first dimension representing actual values and the
+ * second dimension values from {@link UserData}.
+ *
+ * {@hide}
+ */
+ @Nullable
+ @SystemApi
+ public float[][] onGetScores(@Nullable String algorithm,
+ @Nullable Bundle args, @NonNull List<AutofillValue> actualValues,
+ @NonNull List<String> userDataValues) {
+ Log.e(TAG, "service implementation (" + getClass() + " does not implement onGetScore()");
+ return null;
+ }
+
+ private final class AutofillFieldClassificationServiceWrapper
+ extends IAutofillFieldClassificationService.Stub {
+ @Override
+ public void getScores(RemoteCallback callback, String algorithmName, Bundle algorithmArgs,
+ List<AutofillValue> actualValues, String[] userDataValues)
+ throws RemoteException {
+ // TODO(b/70939974): refactor to use PooledLambda
+ mHandlerCaller.obtainMessageOOOOO(MSG_GET_SCORES, callback, algorithmName,
+ algorithmArgs, actualValues, userDataValues).sendToTarget();
+ }
+ }
+
+ /**
+ * Helper class used to encapsulate a float[][] in a Parcelable.
+ *
+ * {@hide}
+ */
+ public static final class Scores implements Parcelable {
+ @NonNull
+ public final float[][] scores;
+
+ private Scores(Parcel parcel) {
+ final int size1 = parcel.readInt();
+ final int size2 = parcel.readInt();
+ scores = new float[size1][size2];
+ for (int i = 0; i < size1; i++) {
+ for (int j = 0; j < size2; j++) {
+ scores[i][j] = parcel.readFloat();
+ }
+ }
+ }
+
+ private Scores(@NonNull float[][] scores) {
+ this.scores = scores;
+ }
+
+ @Override
+ public String toString() {
+ final int size1 = scores.length;
+ final int size2 = size1 > 0 ? scores[0].length : 0;
+ final StringBuilder builder = new StringBuilder("Scores [")
+ .append(size1).append("x").append(size2).append("] ");
+ for (int i = 0; i < size1; i++) {
+ builder.append(i).append(": ").append(Arrays.toString(scores[i])).append(' ');
+ }
+ return builder.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ int size1 = scores.length;
+ int size2 = scores[0].length;
+ parcel.writeInt(size1);
+ parcel.writeInt(size2);
+ for (int i = 0; i < size1; i++) {
+ for (int j = 0; j < size2; j++) {
+ parcel.writeFloat(scores[i][j]);
+ }
+ }
+ }
+
+ public static final Creator<Scores> CREATOR = new Creator<Scores>() {
+ @Override
+ public Scores createFromParcel(Parcel parcel) {
+ return new Scores(parcel);
+ }
+
+ @Override
+ public Scores[] newArray(int size) {
+ return new Scores[size];
+ }
+ };
+ }
+}
diff --git a/android/service/autofill/CharSequenceTransformation.java b/android/service/autofill/CharSequenceTransformation.java
index 2413e97..f52ac85 100644
--- a/android/service/autofill/CharSequenceTransformation.java
+++ b/android/service/autofill/CharSequenceTransformation.java
@@ -22,7 +22,6 @@
import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
-import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import android.view.autofill.AutofillId;
@@ -31,6 +30,8 @@
import com.android.internal.util.Preconditions;
+import java.util.LinkedHashMap;
+import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -62,7 +63,9 @@
public final class CharSequenceTransformation extends InternalTransformation implements
Transformation, Parcelable {
private static final String TAG = "CharSequenceTransformation";
- @NonNull private final ArrayMap<AutofillId, Pair<Pattern, String>> mFields;
+
+ // Must use LinkedHashMap to preserve insertion order.
+ @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields;
private CharSequenceTransformation(Builder builder) {
mFields = builder.mFields;
@@ -76,9 +79,9 @@
final StringBuilder converted = new StringBuilder();
final int size = mFields.size();
if (sDebug) Log.d(TAG, size + " multiple fields on id " + childViewId);
- for (int i = 0; i < size; i++) {
- final AutofillId id = mFields.keyAt(i);
- final Pair<Pattern, String> field = mFields.valueAt(i);
+ for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
+ final AutofillId id = entry.getKey();
+ final Pair<Pattern, String> field = entry.getValue();
final String value = finder.findByAutofillId(id);
if (value == null) {
Log.w(TAG, "No value for id " + id);
@@ -107,8 +110,10 @@
* Builder for {@link CharSequenceTransformation} objects.
*/
public static class Builder {
- @NonNull private final ArrayMap<AutofillId, Pair<Pattern, String>> mFields =
- new ArrayMap<>();
+
+ // Must use LinkedHashMap to preserve insertion order.
+ @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields =
+ new LinkedHashMap<>();
private boolean mDestroyed;
/**
@@ -186,12 +191,15 @@
final Pattern[] regexs = new Pattern[size];
final String[] substs = new String[size];
Pair<Pattern, String> pair;
- for (int i = 0; i < size; i++) {
- ids[i] = mFields.keyAt(i);
- pair = mFields.valueAt(i);
+ int i = 0;
+ for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
+ ids[i] = entry.getKey();
+ pair = entry.getValue();
regexs[i] = pair.first;
substs[i] = pair.second;
+ i++;
}
+
parcel.writeParcelableArray(ids, flags);
parcel.writeSerializable(regexs);
parcel.writeStringArray(substs);
diff --git a/android/service/autofill/EditDistanceScorer.java b/android/service/autofill/EditDistanceScorer.java
deleted file mode 100644
index 0706b37..0000000
--- a/android/service/autofill/EditDistanceScorer.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2017 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.service.autofill;
-
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.view.autofill.AutofillValue;
-
-/**
- * Helper used to calculate the classification score between an actual {@link AutofillValue} filled
- * by the user and the expected value predicted by an autofill service.
- */
-// TODO(b/70291841): explain algorithm once it's fully implemented
-public final class EditDistanceScorer extends InternalScorer implements Scorer, Parcelable {
-
- private static final EditDistanceScorer sInstance = new EditDistanceScorer();
-
- /**
- * Gets the singleton instance.
- */
- public static EditDistanceScorer getInstance() {
- return sInstance;
- }
-
- private EditDistanceScorer() {
- }
-
- /** @hide */
- @Override
- public float getScore(@NonNull AutofillValue actualValue, @NonNull String userData) {
- if (actualValue == null || !actualValue.isText() || userData == null) return 0;
- // TODO(b/70291841): implement edit distance - currently it's returning either 0, 100%, or
- // partial match when number of chars match
- final String textValue = actualValue.getTextValue().toString();
- final int total = textValue.length();
- if (total != userData.length()) return 0F;
-
- int matches = 0;
- for (int i = 0; i < total; i++) {
- if (Character.toLowerCase(textValue.charAt(i)) == Character
- .toLowerCase(userData.charAt(i))) {
- matches++;
- }
- }
-
- return ((float) matches) / total;
- }
-
- /////////////////////////////////////
- // Object "contract" methods. //
- /////////////////////////////////////
- @Override
- public String toString() {
- return "EditDistanceScorer";
- }
-
- /////////////////////////////////////
- // Parcelable "contract" methods. //
- /////////////////////////////////////
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel parcel, int flags) {
- // Do nothing
- }
-
- public static final Parcelable.Creator<EditDistanceScorer> CREATOR =
- new Parcelable.Creator<EditDistanceScorer>() {
- @Override
- public EditDistanceScorer createFromParcel(Parcel parcel) {
- return EditDistanceScorer.getInstance();
- }
-
- @Override
- public EditDistanceScorer[] newArray(int size) {
- return new EditDistanceScorer[size];
- }
- };
-}
diff --git a/android/service/autofill/FieldClassification.java b/android/service/autofill/FieldClassification.java
index 001b291..cd1efd6 100644
--- a/android/service/autofill/FieldClassification.java
+++ b/android/service/autofill/FieldClassification.java
@@ -105,9 +105,6 @@
/**
* Represents the score of a {@link UserData} entry for the field.
- *
- * <p>The score is defined by {@link #getScore()} and the entry is identified by
- * {@link #getRemoteId()}.
*/
public static final class Match {
@@ -140,8 +137,9 @@
* <li>Any other value is a partial match.
* </ul>
*
- * <p>How the score is calculated depends on the algorithm used by the {@link Scorer}
- * implementation.
+ * <p>How the score is calculated depends on the
+ * {@link UserData.Builder#setFieldClassificationAlgorithm(String, android.os.Bundle)
+ * algorithm} used.
*/
public float getScore() {
return mScore;
diff --git a/android/service/autofill/InternalScorer.java b/android/service/autofill/InternalScorer.java
deleted file mode 100644
index 0da5afc..0000000
--- a/android/service/autofill/InternalScorer.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2017 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.service.autofill;
-
-import android.annotation.NonNull;
-import android.annotation.TestApi;
-import android.os.Parcelable;
-import android.view.autofill.AutofillValue;
-
-/**
- * Superclass of all scorer the system understands. As this is not public all
- * subclasses have to implement {@link Scorer} again.
- *
- * @hide
- */
-@TestApi
-public abstract class InternalScorer implements Scorer, Parcelable {
-
- /**
- * Returns the classification score between an actual {@link AutofillValue} filled
- * by the user and the expected value predicted by an autofill service.
- *
- * <p>A full-match is {@code 1.0} (representing 100%), a full mismatch is {@code 0.0} and
- * partial mathces are something in between, typically using edit-distance algorithms.
- */
- public abstract float getScore(@NonNull AutofillValue actualValue, @NonNull String userData);
-}
diff --git a/android/service/autofill/Scorer.java b/android/service/autofill/Scorer.java
deleted file mode 100644
index c401855..0000000
--- a/android/service/autofill/Scorer.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2017 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.service.autofill;
-
-/**
- * Helper class used to calculate a score.
- *
- * <p>Typically used to calculate the
- * <a href="AutofillService.html#FieldClassification">field classification</a> score between an
- * actual {@link android.view.autofill.AutofillValue} filled by the user and the expected value
- * predicted by an autofill service.
- */
-public interface Scorer {
-
-}
diff --git a/android/service/autofill/UserData.java b/android/service/autofill/UserData.java
index f0cc360..9017848 100644
--- a/android/service/autofill/UserData.java
+++ b/android/service/autofill/UserData.java
@@ -25,10 +25,13 @@
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.content.ContentResolver;
+import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.Settings;
+import android.service.autofill.FieldClassification.Match;
import android.util.Log;
+import android.view.autofill.AutofillManager;
import android.view.autofill.Helper;
import com.android.internal.util.Preconditions;
@@ -49,21 +52,32 @@
private static final int DEFAULT_MIN_VALUE_LENGTH = 5;
private static final int DEFAULT_MAX_VALUE_LENGTH = 100;
- private final InternalScorer mScorer;
+ private final String mAlgorithm;
+ private final Bundle mAlgorithmArgs;
private final String[] mRemoteIds;
private final String[] mValues;
private UserData(Builder builder) {
- mScorer = builder.mScorer;
+ mAlgorithm = builder.mAlgorithm;
+ mAlgorithmArgs = builder.mAlgorithmArgs;
mRemoteIds = new String[builder.mRemoteIds.size()];
builder.mRemoteIds.toArray(mRemoteIds);
mValues = new String[builder.mValues.size()];
builder.mValues.toArray(mValues);
}
+ /**
+ * Gets the name of the algorithm that is used to calculate
+ * {@link Match#getScore() match scores}.
+ */
+ @Nullable
+ public String getFieldClassificationAlgorithm() {
+ return mAlgorithm;
+ }
+
/** @hide */
- public InternalScorer getScorer() {
- return mScorer;
+ public Bundle getAlgorithmArgs() {
+ return mAlgorithmArgs;
}
/** @hide */
@@ -78,7 +92,9 @@
/** @hide */
public void dump(String prefix, PrintWriter pw) {
- pw.print(prefix); pw.print("Scorer: "); pw.println(mScorer);
+ pw.print(prefix); pw.print("Algorithm: "); pw.print(mAlgorithm);
+ pw.print(" Args: "); pw.println(mAlgorithmArgs);
+
// Cannot disclose remote ids or values because they could contain PII
pw.print(prefix); pw.print("Remote ids size: "); pw.println(mRemoteIds.length);
for (int i = 0; i < mRemoteIds.length; i++) {
@@ -105,9 +121,10 @@
* A builder for {@link UserData} objects.
*/
public static final class Builder {
- private final InternalScorer mScorer;
private final ArrayList<String> mRemoteIds;
private final ArrayList<String> mValues;
+ private String mAlgorithm;
+ private Bundle mAlgorithmArgs;
private boolean mDestroyed;
/**
@@ -120,13 +137,9 @@
* <li>{@code value} is empty
* <li>the length of {@code value} is lower than {@link UserData#getMinValueLength()}
* <li>the length of {@code value} is higher than {@link UserData#getMaxValueLength()}
- * <li>{@code scorer} is not instance of a class provided by the Android System.
* </ol>
*/
- public Builder(@NonNull Scorer scorer, @NonNull String remoteId, @NonNull String value) {
- Preconditions.checkArgument((scorer instanceof InternalScorer),
- "not provided by Android System: " + scorer);
- mScorer = (InternalScorer) scorer;
+ public Builder(@NonNull String remoteId, @NonNull String value) {
checkValidRemoteId(remoteId);
checkValidValue(value);
final int capacity = getMaxUserDataSize();
@@ -137,6 +150,28 @@
}
/**
+ * Sets the algorithm used for <a href="#FieldClassification">field classification</a>.
+ *
+ * <p>The currently available algorithms can be retrieve through
+ * {@link AutofillManager#getAvailableFieldClassificationAlgorithms()}.
+ *
+ * <p>If not set, the
+ * {@link AutofillManager#getDefaultFieldClassificationAlgorithm() default algorithm} is
+ * used instead.
+ *
+ * @param name name of the algorithm or {@code null} to used default.
+ * @param args optional arguments to the algorithm.
+ *
+ * @return this builder
+ */
+ public Builder setFieldClassificationAlgorithm(@Nullable String name,
+ @Nullable Bundle args) {
+ mAlgorithm = name;
+ mAlgorithmArgs = args;
+ return this;
+ }
+
+ /**
* Adds a new value for user data.
*
* @param remoteId unique string used to identify the user data.
@@ -211,7 +246,7 @@
public String toString() {
if (!sDebug) return super.toString();
- final StringBuilder builder = new StringBuilder("UserData: [scorer=").append(mScorer);
+ final StringBuilder builder = new StringBuilder("UserData: [algorithm=").append(mAlgorithm);
// Cannot disclose remote ids or values because they could contain PII
builder.append(", remoteIds=");
Helper.appendRedacted(builder, mRemoteIds);
@@ -231,9 +266,10 @@
@Override
public void writeToParcel(Parcel parcel, int flags) {
- parcel.writeParcelable(mScorer, flags);
parcel.writeStringArray(mRemoteIds);
parcel.writeStringArray(mValues);
+ parcel.writeString(mAlgorithm);
+ parcel.writeBundle(mAlgorithmArgs);
}
public static final Parcelable.Creator<UserData> CREATOR =
@@ -243,10 +279,10 @@
// Always go through the builder to ensure the data ingested by
// the system obeys the contract of the builder to avoid attacks
// using specially crafted parcels.
- final InternalScorer scorer = parcel.readParcelable(null);
final String[] remoteIds = parcel.readStringArray();
final String[] values = parcel.readStringArray();
- final Builder builder = new Builder(scorer, remoteIds[0], values[0]);
+ final Builder builder = new Builder(remoteIds[0], values[0])
+ .setFieldClassificationAlgorithm(parcel.readString(), parcel.readBundle());
for (int i = 1; i < remoteIds.length; i++) {
builder.add(remoteIds[i], values[i]);
}
diff --git a/android/service/carrier/CarrierIdentifier.java b/android/service/carrier/CarrierIdentifier.java
index b47e872..09bba4b 100644
--- a/android/service/carrier/CarrierIdentifier.java
+++ b/android/service/carrier/CarrierIdentifier.java
@@ -16,9 +16,14 @@
package android.service.carrier;
+import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.internal.telephony.uicc.IccUtils;
+
+import java.util.Objects;
+
/**
* Used to pass info to CarrierConfigService implementations so they can decide what values to
* return.
@@ -40,13 +45,13 @@
private String mMcc;
private String mMnc;
- private String mSpn;
- private String mImsi;
- private String mGid1;
- private String mGid2;
+ private @Nullable String mSpn;
+ private @Nullable String mImsi;
+ private @Nullable String mGid1;
+ private @Nullable String mGid2;
- public CarrierIdentifier(String mcc, String mnc, String spn, String imsi, String gid1,
- String gid2) {
+ public CarrierIdentifier(String mcc, String mnc, @Nullable String spn, @Nullable String imsi,
+ @Nullable String gid1, @Nullable String gid2) {
mMcc = mcc;
mMnc = mnc;
mSpn = spn;
@@ -55,6 +60,32 @@
mGid2 = gid2;
}
+ /**
+ * Creates a carrier identifier instance.
+ *
+ * @param mccMnc A 3-byte array as defined by 3GPP TS 24.008.
+ * @param gid1 The group identifier level 1.
+ * @param gid2 The group identifier level 2.
+ * @throws IllegalArgumentException If the length of {@code mccMnc} is not 3.
+ */
+ public CarrierIdentifier(byte[] mccMnc, @Nullable String gid1, @Nullable String gid2) {
+ if (mccMnc.length != 3) {
+ throw new IllegalArgumentException(
+ "MCC & MNC must be set by a 3-byte array: byte[" + mccMnc.length + "]");
+ }
+ String hex = IccUtils.bytesToHexString(mccMnc);
+ mMcc = new String(new char[] {hex.charAt(1), hex.charAt(0), hex.charAt(3)});
+ if (hex.charAt(2) == 'F') {
+ mMnc = new String(new char[] {hex.charAt(5), hex.charAt(4)});
+ } else {
+ mMnc = new String(new char[] {hex.charAt(5), hex.charAt(4), hex.charAt(2)});
+ }
+ mGid1 = gid1;
+ mGid2 = gid2;
+ mSpn = null;
+ mImsi = null;
+ }
+
/** @hide */
public CarrierIdentifier(Parcel parcel) {
readFromParcel(parcel);
@@ -71,26 +102,60 @@
}
/** Get the service provider name. */
+ @Nullable
public String getSpn() {
return mSpn;
}
/** Get the international mobile subscriber identity. */
+ @Nullable
public String getImsi() {
return mImsi;
}
/** Get the group identifier level 1. */
+ @Nullable
public String getGid1() {
return mGid1;
}
/** Get the group identifier level 2. */
+ @Nullable
public String getGid2() {
return mGid2;
}
@Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ CarrierIdentifier that = (CarrierIdentifier) obj;
+ return Objects.equals(mMcc, that.mMcc)
+ && Objects.equals(mMnc, that.mMnc)
+ && Objects.equals(mSpn, that.mSpn)
+ && Objects.equals(mImsi, that.mImsi)
+ && Objects.equals(mGid1, that.mGid1)
+ && Objects.equals(mGid2, that.mGid2);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + Objects.hashCode(mMcc);
+ result = 31 * result + Objects.hashCode(mMnc);
+ result = 31 * result + Objects.hashCode(mSpn);
+ result = 31 * result + Objects.hashCode(mImsi);
+ result = 31 * result + Objects.hashCode(mGid1);
+ result = 31 * result + Objects.hashCode(mGid2);
+ return result;
+ }
+
+ @Override
public int describeContents() {
return 0;
}
diff --git a/android/service/dreams/DreamService.java b/android/service/dreams/DreamService.java
index 2a245d0..99e2c62 100644
--- a/android/service/dreams/DreamService.java
+++ b/android/service/dreams/DreamService.java
@@ -17,6 +17,7 @@
import android.annotation.IdRes;
import android.annotation.LayoutRes;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
@@ -54,7 +55,6 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.util.List;
/**
* Extend this class to implement a custom dream (available to the user as a "Daydream").
@@ -458,8 +458,16 @@
* was processed in {@link #onCreate}.
*
* <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p>
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
*
+ * @param id the ID to search for
* @return The view if found or null otherwise.
+ * @see View#findViewById(int)
+ * @see DreamService#requireViewById(int)
*/
@Nullable
public <T extends View> T findViewById(@IdRes int id) {
@@ -467,6 +475,33 @@
}
/**
+ * Finds a view that was identified by the id attribute from the XML that was processed in
+ * {@link #onCreate}, or throws an IllegalArgumentException if the ID is invalid or there is no
+ * matching view in the hierarchy.
+ *
+ * <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p>
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see View#requireViewById(int)
+ * @see DreamService#findViewById(int)
+ */
+ @NonNull
+ public final <T extends View> T requireViewById(@IdRes int id) {
+ T view = findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException(
+ "ID does not reference a View inside this DreamService");
+ }
+ return view;
+ }
+
+ /**
* Marks this dream as interactive to receive input events.
*
* <p>Non-interactive dreams (default) will dismiss on the first input event.</p>
diff --git a/android/service/euicc/EuiccProfileInfo.java b/android/service/euicc/EuiccProfileInfo.java
index ba6c9a2..8e752d1 100644
--- a/android/service/euicc/EuiccProfileInfo.java
+++ b/android/service/euicc/EuiccProfileInfo.java
@@ -15,12 +15,19 @@
*/
package android.service.euicc;
+import android.annotation.IntDef;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
+import android.service.carrier.CarrierIdentifier;
import android.telephony.UiccAccessRule;
import android.text.TextUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+
/**
* Information about an embedded profile (subscription) on an eUICC.
*
@@ -30,18 +37,90 @@
*/
public final class EuiccProfileInfo implements Parcelable {
+ /** Profile policy rules (bit mask) */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "POLICY_RULE_" }, value = {
+ POLICY_RULE_DO_NOT_DISABLE,
+ POLICY_RULE_DO_NOT_DELETE,
+ POLICY_RULE_DELETE_AFTER_DISABLING
+ })
+ public @interface PolicyRule {}
+ /** Once this profile is enabled, it cannot be disabled. */
+ public static final int POLICY_RULE_DO_NOT_DISABLE = 1;
+ /** This profile cannot be deleted. */
+ public static final int POLICY_RULE_DO_NOT_DELETE = 1 << 1;
+ /** This profile should be deleted after being disabled. */
+ public static final int POLICY_RULE_DELETE_AFTER_DISABLING = 1 << 2;
+
+ /** Class of the profile */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "PROFILE_CLASS_" }, value = {
+ PROFILE_CLASS_TESTING,
+ PROFILE_CLASS_PROVISIONING,
+ PROFILE_CLASS_OPERATIONAL,
+ PROFILE_CLASS_UNSET
+ })
+ public @interface ProfileClass {}
+ /** Testing profiles */
+ public static final int PROFILE_CLASS_TESTING = 0;
+ /** Provisioning profiles which are pre-loaded on eUICC */
+ public static final int PROFILE_CLASS_PROVISIONING = 1;
+ /** Operational profiles which can be pre-loaded or downloaded */
+ public static final int PROFILE_CLASS_OPERATIONAL = 2;
+ /**
+ * Profile class not set.
+ * @hide
+ */
+ public static final int PROFILE_CLASS_UNSET = -1;
+
+ /** State of the profile */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "PROFILE_STATE_" }, value = {
+ PROFILE_STATE_DISABLED,
+ PROFILE_STATE_ENABLED,
+ PROFILE_STATE_UNSET
+ })
+ public @interface ProfileState {}
+ /** Disabled profiles */
+ public static final int PROFILE_STATE_DISABLED = 0;
+ /** Enabled profile */
+ public static final int PROFILE_STATE_ENABLED = 1;
+ /**
+ * Profile state not set.
+ * @hide
+ */
+ public static final int PROFILE_STATE_UNSET = -1;
+
/** The iccid of the subscription. */
public final String iccid;
+ /** An optional nickname for the subscription. */
+ public final @Nullable String nickname;
+
+ /** The service provider name for the subscription. */
+ public final String serviceProviderName;
+
+ /** The profile name for the subscription. */
+ public final String profileName;
+
+ /** Profile class for the subscription. */
+ @ProfileClass public final int profileClass;
+
+ /** The profile state of the subscription. */
+ @ProfileState public final int state;
+
+ /** The operator Id of the subscription. */
+ public final CarrierIdentifier carrierIdentifier;
+
+ /** The policy rules of the subscription. */
+ @PolicyRule public final int policyRules;
+
/**
* Optional access rules defining which apps can manage this subscription. If unset, only the
* platform can manage it.
*/
public final @Nullable UiccAccessRule[] accessRules;
- /** An optional nickname for the subscription. */
- public final @Nullable String nickname;
-
public static final Creator<EuiccProfileInfo> CREATOR = new Creator<EuiccProfileInfo>() {
@Override
public EuiccProfileInfo createFromParcel(Parcel in) {
@@ -54,6 +133,12 @@
}
};
+ // TODO(b/70292228): Remove this method when LPA can be updated.
+ /**
+ * @hide
+ * @deprecated - Do not use.
+ */
+ @Deprecated
public EuiccProfileInfo(String iccid, @Nullable UiccAccessRule[] accessRules,
@Nullable String nickname) {
if (!TextUtils.isDigitsOnly(iccid)) {
@@ -62,23 +147,290 @@
this.iccid = iccid;
this.accessRules = accessRules;
this.nickname = nickname;
+
+ this.serviceProviderName = null;
+ this.profileName = null;
+ this.profileClass = PROFILE_CLASS_UNSET;
+ this.state = PROFILE_CLASS_UNSET;
+ this.carrierIdentifier = null;
+ this.policyRules = 0;
}
private EuiccProfileInfo(Parcel in) {
iccid = in.readString();
- accessRules = in.createTypedArray(UiccAccessRule.CREATOR);
nickname = in.readString();
+ serviceProviderName = in.readString();
+ profileName = in.readString();
+ profileClass = in.readInt();
+ state = in.readInt();
+ byte exist = in.readByte();
+ if (exist == (byte) 1) {
+ carrierIdentifier = CarrierIdentifier.CREATOR.createFromParcel(in);
+ } else {
+ carrierIdentifier = null;
+ }
+ policyRules = in.readInt();
+ accessRules = in.createTypedArray(UiccAccessRule.CREATOR);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(iccid);
- dest.writeTypedArray(accessRules, flags);
dest.writeString(nickname);
+ dest.writeString(serviceProviderName);
+ dest.writeString(profileName);
+ dest.writeInt(profileClass);
+ dest.writeInt(state);
+ if (carrierIdentifier != null) {
+ dest.writeByte((byte) 1);
+ carrierIdentifier.writeToParcel(dest, flags);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ dest.writeInt(policyRules);
+ dest.writeTypedArray(accessRules, flags);
}
@Override
public int describeContents() {
return 0;
}
+
+ /** The builder to build a new {@link EuiccProfileInfo} instance. */
+ public static final class Builder {
+ public String iccid;
+ public UiccAccessRule[] accessRules;
+ public String nickname;
+ public String serviceProviderName;
+ public String profileName;
+ @ProfileClass public int profileClass;
+ @ProfileState public int state;
+ public CarrierIdentifier carrierIdentifier;
+ @PolicyRule public int policyRules;
+
+ public Builder() {}
+
+ public Builder(EuiccProfileInfo baseProfile) {
+ iccid = baseProfile.iccid;
+ nickname = baseProfile.nickname;
+ serviceProviderName = baseProfile.serviceProviderName;
+ profileName = baseProfile.profileName;
+ profileClass = baseProfile.profileClass;
+ state = baseProfile.state;
+ carrierIdentifier = baseProfile.carrierIdentifier;
+ policyRules = baseProfile.policyRules;
+ accessRules = baseProfile.accessRules;
+ }
+
+ /** Builds the profile instance. */
+ public EuiccProfileInfo build() {
+ if (iccid == null) {
+ throw new IllegalStateException("ICCID must be set for a profile.");
+ }
+ return new EuiccProfileInfo(
+ iccid,
+ nickname,
+ serviceProviderName,
+ profileName,
+ profileClass,
+ state,
+ carrierIdentifier,
+ policyRules,
+ accessRules);
+ }
+
+ /** Sets the iccId of the subscription. */
+ public Builder setIccid(String value) {
+ if (!TextUtils.isDigitsOnly(value)) {
+ throw new IllegalArgumentException("iccid contains invalid characters: " + value);
+ }
+ iccid = value;
+ return this;
+ }
+
+ /** Sets the nickname of the subscription. */
+ public Builder setNickname(String value) {
+ nickname = value;
+ return this;
+ }
+
+ /** Sets the service provider name of the subscription. */
+ public Builder setServiceProviderName(String value) {
+ serviceProviderName = value;
+ return this;
+ }
+
+ /** Sets the profile name of the subscription. */
+ public Builder setProfileName(String value) {
+ profileName = value;
+ return this;
+ }
+
+ /** Sets the profile class of the subscription. */
+ public Builder setProfileClass(@ProfileClass int value) {
+ profileClass = value;
+ return this;
+ }
+
+ /** Sets the state of the subscription. */
+ public Builder setState(@ProfileState int value) {
+ state = value;
+ return this;
+ }
+
+ /** Sets the carrier identifier of the subscription. */
+ public Builder setCarrierIdentifier(CarrierIdentifier value) {
+ carrierIdentifier = value;
+ return this;
+ }
+
+ /** Sets the policy rules of the subscription. */
+ public Builder setPolicyRules(@PolicyRule int value) {
+ policyRules = value;
+ return this;
+ }
+
+ /** Sets the access rules of the subscription. */
+ public Builder setUiccAccessRule(@Nullable UiccAccessRule[] value) {
+ accessRules = value;
+ return this;
+ }
+ }
+
+ private EuiccProfileInfo(
+ String iccid,
+ @Nullable String nickname,
+ String serviceProviderName,
+ String profileName,
+ @ProfileClass int profileClass,
+ @ProfileState int state,
+ CarrierIdentifier carrierIdentifier,
+ @PolicyRule int policyRules,
+ @Nullable UiccAccessRule[] accessRules) {
+ this.iccid = iccid;
+ this.nickname = nickname;
+ this.serviceProviderName = serviceProviderName;
+ this.profileName = profileName;
+ this.profileClass = profileClass;
+ this.state = state;
+ this.carrierIdentifier = carrierIdentifier;
+ this.policyRules = policyRules;
+ this.accessRules = accessRules;
+ }
+
+ /** Gets the ICCID string. */
+ public String getIccid() {
+ return iccid;
+ }
+
+ /** Gets the access rules. */
+ @Nullable
+ public UiccAccessRule[] getUiccAccessRules() {
+ return accessRules;
+ }
+
+ /** Gets the nickname. */
+ public String getNickname() {
+ return nickname;
+ }
+
+ /** Gets the service provider name. */
+ public String getServiceProviderName() {
+ return serviceProviderName;
+ }
+
+ /** Gets the profile name. */
+ public String getProfileName() {
+ return profileName;
+ }
+
+ /** Gets the profile class. */
+ @ProfileClass
+ public int getProfileClass() {
+ return profileClass;
+ }
+
+ /** Gets the state of the subscription. */
+ @ProfileState
+ public int getState() {
+ return state;
+ }
+
+ /** Gets the carrier identifier. */
+ public CarrierIdentifier getCarrierIdentifier() {
+ return carrierIdentifier;
+ }
+
+ /** Gets the policy rules. */
+ @PolicyRule
+ public int getPolicyRules() {
+ return policyRules;
+ }
+
+ /** Returns whether any policy rule exists. */
+ public boolean hasPolicyRules() {
+ return policyRules != 0;
+ }
+
+ /** Checks whether a certain policy rule exists. */
+ public boolean hasPolicyRule(@PolicyRule int policy) {
+ return (policyRules & policy) != 0;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ EuiccProfileInfo that = (EuiccProfileInfo) obj;
+ return Objects.equals(iccid, that.iccid)
+ && Objects.equals(nickname, that.nickname)
+ && Objects.equals(serviceProviderName, that.serviceProviderName)
+ && Objects.equals(profileName, that.profileName)
+ && profileClass == that.profileClass
+ && state == that.state
+ && Objects.equals(carrierIdentifier, that.carrierIdentifier)
+ && policyRules == that.policyRules
+ && Arrays.equals(accessRules, that.accessRules);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + Objects.hashCode(iccid);
+ result = 31 * result + Objects.hashCode(nickname);
+ result = 31 * result + Objects.hashCode(serviceProviderName);
+ result = 31 * result + Objects.hashCode(profileName);
+ result = 31 * result + profileClass;
+ result = 31 * result + state;
+ result = 31 * result + Objects.hashCode(carrierIdentifier);
+ result = 31 * result + policyRules;
+ result = 31 * result + Arrays.hashCode(accessRules);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "EuiccProfileInfo (nickname="
+ + nickname
+ + ", serviceProviderName="
+ + serviceProviderName
+ + ", profileName="
+ + profileName
+ + ", profileClass="
+ + profileClass
+ + ", state="
+ + state
+ + ", CarrierIdentifier="
+ + carrierIdentifier.toString()
+ + ", policyRules="
+ + policyRules
+ + ", accessRules="
+ + Arrays.toString(accessRules)
+ + ")";
+ }
}
diff --git a/android/service/euicc/EuiccService.java b/android/service/euicc/EuiccService.java
index fb53007..be85800 100644
--- a/android/service/euicc/EuiccService.java
+++ b/android/service/euicc/EuiccService.java
@@ -193,6 +193,18 @@
}
/**
+ * Callback class for {@link #onStartOtaIfNecessary(int, OtaStatusChangedCallback)}
+ *
+ * The status of OTA which can be {@code android.telephony.euicc.EuiccManager#EUICC_OTA_}
+ *
+ * @see IEuiccService#startOtaIfNecessary
+ */
+ public interface OtaStatusChangedCallback {
+ /** Called when OTA status is changed. */
+ void onOtaStatusChanged(int status);
+ }
+
+ /**
* Return the EID of the eUICC.
*
* @param slotId ID of the SIM slot being queried. This is currently not populated but is here
@@ -214,6 +226,16 @@
public abstract @OtaStatus int onGetOtaStatus(int slotId);
/**
+ * Perform OTA if current OS is not the latest one.
+ *
+ * @param slotId ID of the SIM slot to use for the operation. This is currently not populated
+ * but is here to future-proof the APIs.
+ * @param statusChangedCallback Function called when OTA status changed.
+ */
+ public abstract void onStartOtaIfNecessary(
+ int slotId, OtaStatusChangedCallback statusChangedCallback);
+
+ /**
* Populate {@link DownloadableSubscription} metadata for the given downloadable subscription.
*
* @param slotId ID of the SIM slot to use for the operation. This is currently not populated
@@ -396,6 +418,26 @@
}
@Override
+ public void startOtaIfNecessary(
+ int slotId, IOtaStatusChangedCallback statusChangedCallback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ EuiccService.this.onStartOtaIfNecessary(slotId, new OtaStatusChangedCallback() {
+ @Override
+ public void onOtaStatusChanged(int status) {
+ try {
+ statusChangedCallback.onOtaStatusChanged(status);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+ });
+ }
+
+ @Override
public void getOtaStatus(int slotId, IGetOtaStatusCallback callback) {
mExecutor.execute(new Runnable() {
@Override
diff --git a/android/service/notification/NotificationListenerService.java b/android/service/notification/NotificationListenerService.java
index 18d4a1e..b7b2b2d 100644
--- a/android/service/notification/NotificationListenerService.java
+++ b/android/service/notification/NotificationListenerService.java
@@ -55,6 +55,7 @@
import android.widget.RemoteViews;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import java.lang.annotation.Retention;
@@ -890,6 +891,8 @@
createLegacyIconExtras(notification);
// populate remote views for older clients.
maybePopulateRemoteViews(notification);
+ // populate people for older clients.
+ maybePopulatePeople(notification);
} catch (IllegalArgumentException e) {
if (corruptNotifications == null) {
corruptNotifications = new ArrayList<>(N);
@@ -1178,6 +1181,25 @@
}
}
+ /**
+ * Populates remote views for pre-P targeting apps.
+ */
+ private void maybePopulatePeople(Notification notification) {
+ if (getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P) {
+ ArrayList<Notification.Person> people = notification.extras.getParcelableArrayList(
+ Notification.EXTRA_PEOPLE_LIST);
+ if (people != null && people.isEmpty()) {
+ int size = people.size();
+ String[] peopleArray = new String[size];
+ for (int i = 0; i < size; i++) {
+ Notification.Person person = people.get(i);
+ peopleArray[i] = person.resolveToLegacyUri();
+ }
+ notification.extras.putStringArray(Notification.EXTRA_PEOPLE, peopleArray);
+ }
+ }
+ }
+
/** @hide */
protected class NotificationListenerWrapper extends INotificationListener.Stub {
@Override
@@ -1522,7 +1544,11 @@
return mShowBadge;
}
- private void populate(String key, int rank, boolean matchesInterruptionFilter,
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public void populate(String key, int rank, boolean matchesInterruptionFilter,
int visibilityOverride, int suppressedVisualEffects, int importance,
CharSequence explanation, String overrideGroupKey,
NotificationChannel channel, ArrayList<String> overridePeople,
diff --git a/android/service/notification/NotifyingApp.java b/android/service/notification/NotifyingApp.java
new file mode 100644
index 0000000..38f18c6
--- /dev/null
+++ b/android/service/notification/NotifyingApp.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2018 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.service.notification;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * @hide
+ */
+public final class NotifyingApp implements Parcelable, Comparable<NotifyingApp> {
+
+ private int mUid;
+ private String mPkg;
+ private long mLastNotified;
+
+ public NotifyingApp() {}
+
+ protected NotifyingApp(Parcel in) {
+ mUid = in.readInt();
+ mPkg = in.readString();
+ mLastNotified = in.readLong();
+ }
+
+ public int getUid() {
+ return mUid;
+ }
+
+ /**
+ * Sets the uid of the package that sent the notification. Returns self.
+ */
+ public NotifyingApp setUid(int mUid) {
+ this.mUid = mUid;
+ return this;
+ }
+
+ public String getPackage() {
+ return mPkg;
+ }
+
+ /**
+ * Sets the package that sent the notification. Returns self.
+ */
+ public NotifyingApp setPackage(@NonNull String mPkg) {
+ this.mPkg = mPkg;
+ return this;
+ }
+
+ public long getLastNotified() {
+ return mLastNotified;
+ }
+
+ /**
+ * Sets the time the notification was originally sent. Returns self.
+ */
+ public NotifyingApp setLastNotified(long mLastNotified) {
+ this.mLastNotified = mLastNotified;
+ return this;
+ }
+
+ public static final Creator<NotifyingApp> CREATOR = new Creator<NotifyingApp>() {
+ @Override
+ public NotifyingApp createFromParcel(Parcel in) {
+ return new NotifyingApp(in);
+ }
+
+ @Override
+ public NotifyingApp[] newArray(int size) {
+ return new NotifyingApp[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mUid);
+ dest.writeString(mPkg);
+ dest.writeLong(mLastNotified);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ NotifyingApp that = (NotifyingApp) o;
+ return getUid() == that.getUid()
+ && getLastNotified() == that.getLastNotified()
+ && Objects.equals(mPkg, that.mPkg);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getUid(), mPkg, getLastNotified());
+ }
+
+ /**
+ * Sorts notifying apps from newest last notified date to oldest.
+ */
+ @Override
+ public int compareTo(NotifyingApp o) {
+ if (getLastNotified() == o.getLastNotified()) {
+ if (getUid() == o.getUid()) {
+ return getPackage().compareTo(o.getPackage());
+ }
+ return Integer.compare(getUid(), o.getUid());
+ }
+
+ return -Long.compare(getLastNotified(), o.getLastNotified());
+ }
+
+ @Override
+ public String toString() {
+ return "NotifyingApp{"
+ + "mUid=" + mUid
+ + ", mPkg='" + mPkg + '\''
+ + ", mLastNotified=" + mLastNotified
+ + '}';
+ }
+}
diff --git a/android/service/trust/TrustAgentService.java b/android/service/trust/TrustAgentService.java
index 4bade9f..40e84b9 100644
--- a/android/service/trust/TrustAgentService.java
+++ b/android/service/trust/TrustAgentService.java
@@ -18,6 +18,7 @@
import android.Manifest;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.SdkConstant;
import android.annotation.SystemApi;
import android.app.Service;
@@ -37,6 +38,7 @@
import android.os.UserManager;
import android.util.Log;
import android.util.Slog;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
@@ -301,7 +303,7 @@
public void onDeviceUnlockLockout(long timeoutMs) {
}
- /**
+ /**
* Called when an escrow token is added for user userId.
*
* @param token the added token
@@ -561,6 +563,31 @@
}
}
+ /**
+ * Request showing a transient error message on the keyguard.
+ * The message will be visible on the lock screen or always on display if possible but can be
+ * overridden by other keyguard events of higher priority - eg. fingerprint auth error.
+ * Other trust agents may override your message if posted simultaneously.
+ *
+ * @param message Message to show.
+ */
+ public final void showKeyguardErrorMessage(@NonNull CharSequence message) {
+ if (message == null) {
+ throw new IllegalArgumentException("message cannot be null");
+ }
+ synchronized (mLock) {
+ if (mCallback == null) {
+ Slog.w(TAG, "Cannot show message because service is not connected to framework.");
+ throw new IllegalStateException("Trust agent is not connected");
+ }
+ try {
+ mCallback.showKeyguardErrorMessage(message);
+ } catch (RemoteException e) {
+ onError("calling showKeyguardErrorMessage");
+ }
+ }
+ }
+
@Override
public final IBinder onBind(Intent intent) {
if (DEBUG) Slog.v(TAG, "onBind() intent = " + intent);
diff --git a/android/service/wallpaper/WallpaperService.java b/android/service/wallpaper/WallpaperService.java
index 595bfb7..8588df7 100644
--- a/android/service/wallpaper/WallpaperService.java
+++ b/android/service/wallpaper/WallpaperService.java
@@ -563,9 +563,12 @@
* Called when the device enters or exits ambient mode.
*
* @param inAmbientMode {@code true} if in ambient mode.
+ * @param animated {@code true} if you'll have te opportunity of animating your transition
+ * {@code false} when the screen will blank and the wallpaper should be
+ * set to ambient mode immediately.
* @hide
*/
- public void onAmbientModeChanged(boolean inAmbientMode) {
+ public void onAmbientModeChanged(boolean inAmbientMode, boolean animated) {
}
/**
@@ -1021,18 +1024,20 @@
* Executes life cycle event and updates internal ambient mode state based on
* message sent from handler.
*
- * @param inAmbientMode True if in ambient mode.
+ * @param inAmbientMode {@code true} if in ambient mode.
+ * @param animated {@code true} if the transition will be animated.
* @hide
*/
@VisibleForTesting
- public void doAmbientModeChanged(boolean inAmbientMode) {
+ public void doAmbientModeChanged(boolean inAmbientMode, boolean animated) {
if (!mDestroyed) {
if (DEBUG) {
- Log.v(TAG, "onAmbientModeChanged(" + inAmbientMode + "): " + this);
+ Log.v(TAG, "onAmbientModeChanged(" + inAmbientMode + ", "
+ + animated + "): " + this);
}
mIsInAmbientMode = inAmbientMode;
if (mCreated) {
- onAmbientModeChanged(inAmbientMode);
+ onAmbientModeChanged(inAmbientMode, animated);
}
}
}
@@ -1278,8 +1283,10 @@
}
@Override
- public void setInAmbientMode(boolean inAmbientDisplay) throws RemoteException {
- Message msg = mCaller.obtainMessageI(DO_IN_AMBIENT_MODE, inAmbientDisplay ? 1 : 0);
+ public void setInAmbientMode(boolean inAmbientDisplay, boolean animated)
+ throws RemoteException {
+ Message msg = mCaller.obtainMessageII(DO_IN_AMBIENT_MODE, inAmbientDisplay ? 1 : 0,
+ animated ? 1 : 0);
mCaller.sendMessage(msg);
}
@@ -1350,7 +1357,7 @@
return;
}
case DO_IN_AMBIENT_MODE: {
- mEngine.doAmbientModeChanged(message.arg1 != 0);
+ mEngine.doAmbientModeChanged(message.arg1 != 0, message.arg2 != 0);
return;
}
case MSG_UPDATE_SURFACE:
diff --git a/android/support/LibraryVersions.java b/android/support/LibraryVersions.java
index 6d8d6bf..813d9a8 100644
--- a/android/support/LibraryVersions.java
+++ b/android/support/LibraryVersions.java
@@ -26,19 +26,14 @@
public static final Version SUPPORT_LIBRARY = new Version("28.0.0-SNAPSHOT");
/**
- * Version code for flatfoot 1.0 projects (room, lifecycles)
- */
- private static final Version FLATFOOT_1_0_BATCH = new Version("1.0.0");
-
- /**
* Version code for Room
*/
- public static final Version ROOM = FLATFOOT_1_0_BATCH;
+ public static final Version ROOM = new Version("1.1.0-alpha1");
/**
* Version code for Lifecycle extensions (ProcessLifecycleOwner, Fragment support)
*/
- public static final Version LIFECYCLES_EXT = new Version("1.1.0-SNAPSHOT");
+ public static final Version LIFECYCLES_EXT = new Version("1.1.0");
/**
* Version code for Lifecycle LiveData
@@ -53,9 +48,9 @@
/**
* Version code for RecyclerView & Room paging
*/
- public static final Version PAGING = new Version("1.0.0-alpha4-1");
+ public static final Version PAGING = new Version("1.0.0-alpha5");
- private static final Version LIFECYCLES = new Version("1.0.3");
+ private static final Version LIFECYCLES = new Version("1.1.0");
/**
* Version code for Lifecycle libs that are required by the support library
@@ -70,15 +65,15 @@
/**
* Version code for shared code of flatfoot
*/
- public static final Version ARCH_CORE = new Version("1.0.0");
+ public static final Version ARCH_CORE = new Version("1.1.0");
/**
* Version code for shared code of flatfoot runtime
*/
- public static final Version ARCH_RUNTIME = FLATFOOT_1_0_BATCH;
+ public static final Version ARCH_RUNTIME = ARCH_CORE;
/**
* Version code for shared testing code of flatfoot
*/
- public static final Version ARCH_CORE_TESTING = FLATFOOT_1_0_BATCH;
+ public static final Version ARCH_CORE_TESTING = ARCH_CORE;
}
diff --git a/android/support/Version.java b/android/support/Version.java
deleted file mode 100644
index 36c7728..0000000
--- a/android/support/Version.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2017 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.support;
-
-import java.io.File;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Utility class which represents a version
- */
-public class Version implements Comparable<Version> {
- private static final Pattern VERSION_FILE_REGEX = Pattern.compile("^(\\d+\\.\\d+\\.\\d+).txt$");
- private static final Pattern VERSION_REGEX = Pattern
- .compile("^(\\d+)\\.(\\d+)\\.(\\d+)(-.+)?$");
-
- private final int mMajor;
- private final int mMinor;
- private final int mPatch;
- private final String mExtra;
-
- public Version(String versionString) {
- this(checkedMatcher(versionString));
- }
-
- private static Matcher checkedMatcher(String versionString) {
- Matcher matcher = VERSION_REGEX.matcher(versionString);
- if (!matcher.matches()) {
- throw new IllegalArgumentException("Can not parse version: " + versionString);
- }
- return matcher;
- }
-
- private Version(Matcher matcher) {
- mMajor = Integer.parseInt(matcher.group(1));
- mMinor = Integer.parseInt(matcher.group(2));
- mPatch = Integer.parseInt(matcher.group(3));
- mExtra = matcher.groupCount() == 4 ? matcher.group(4) : null;
- }
-
- @Override
- public int compareTo(Version version) {
- if (mMajor != version.mMajor) {
- return mMajor - version.mMajor;
- }
- if (mMinor != version.mMinor) {
- return mMinor - version.mMinor;
- }
- if (mPatch != version.mPatch) {
- return mPatch - version.mPatch;
- }
- if (mExtra == null) {
- if (version.mExtra == null) {
- return 0;
- }
- // not having any extra is always a later version
- return 1;
- } else {
- if (version.mExtra == null) {
- // not having any extra is always a later version
- return -1;
- }
- // gradle uses lexicographic ordering
- return mExtra.compareTo(version.mExtra);
- }
- }
-
- public boolean isPatch() {
- return mPatch != 0;
- }
-
- public boolean isSnapshot() {
- return "-SNAPSHOT".equals(mExtra);
- }
-
- public int getMajor() {
- return mMajor;
- }
-
- public int getMinor() {
- return mMinor;
- }
-
- public int getPatch() {
- return mPatch;
- }
-
- public String getExtra() {
- return mExtra;
- }
-
- @Override
- public String toString() {
- return mMajor + "." + mMinor + "." + mPatch + (mExtra != null ? mExtra : "");
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- Version version = (Version) o;
-
- if (mMajor != version.mMajor) return false;
- if (mMinor != version.mMinor) return false;
- if (mPatch != version.mPatch) return false;
- return mExtra != null ? mExtra.equals(version.mExtra) : version.mExtra == null;
- }
-
- @Override
- public int hashCode() {
- int result = mMajor;
- result = 31 * result + mMinor;
- result = 31 * result + mPatch;
- result = 31 * result + (mExtra != null ? mExtra.hashCode() : 0);
- return result;
- }
-
- /**
- * @return Version or null, if a name of the given file doesn't match
- */
- public static Version from(File file) {
- if (!file.isFile()) {
- return null;
- }
- Matcher matcher = VERSION_FILE_REGEX.matcher(file.getName());
- if (!matcher.matches()) {
- return null;
- }
- return new Version(matcher.group(1));
- }
-
- /**
- * @return Version or null, if the given string doesn't match
- */
- public static Version from(String versionString) {
- Matcher matcher = VERSION_REGEX.matcher(versionString);
- if (!matcher.matches()) {
- return null;
- }
- return new Version(matcher);
- }
-}
diff --git a/android/support/VersionFileWriterTask.java b/android/support/VersionFileWriterTask.java
deleted file mode 100644
index aafa023..0000000
--- a/android/support/VersionFileWriterTask.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2017 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.support;
-
-import com.android.build.gradle.LibraryExtension;
-
-import org.gradle.api.Action;
-import org.gradle.api.DefaultTask;
-import org.gradle.api.Project;
-import org.gradle.api.tasks.Input;
-import org.gradle.api.tasks.OutputFile;
-import org.gradle.api.tasks.TaskAction;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.PrintWriter;
-
-/**
- * Task that allows to write a version to a given output file.
- */
-public class VersionFileWriterTask extends DefaultTask {
- public static final String RESOURCE_DIRECTORY = "generatedResources";
- public static final String VERSION_FILE_PATH =
- RESOURCE_DIRECTORY + "/META-INF/%s_%s.version";
-
- private String mVersion;
- private File mOutputFile;
-
- /**
- * Sets up Android Library project to have a task that generates a version file.
- *
- * @param project an Android Library project.
- */
- public static void setUpAndroidLibrary(Project project) {
- project.afterEvaluate(new Action<Project>() {
- @Override
- public void execute(Project project) {
- LibraryExtension library =
- project.getExtensions().findByType(LibraryExtension.class);
-
- String group = (String) project.getProperties().get("group");
- String artifactId = (String) project.getProperties().get("name");
- String version = (String) project.getProperties().get("version");
-
- // Add a java resource file to the library jar for version tracking purposes.
- File artifactName = new File(project.getBuildDir(),
- String.format(VersionFileWriterTask.VERSION_FILE_PATH,
- group, artifactId));
-
- VersionFileWriterTask writeVersionFile =
- project.getTasks().create("writeVersionFile", VersionFileWriterTask.class);
- writeVersionFile.setVersion(version);
- writeVersionFile.setOutputFile(artifactName);
-
- library.getLibraryVariants().all(
- libraryVariant -> libraryVariant.getProcessJavaResources().dependsOn(
- writeVersionFile));
-
- library.getSourceSets().getByName("main").getResources().srcDir(
- new File(project.getBuildDir(), VersionFileWriterTask.RESOURCE_DIRECTORY)
- );
- }
- });
- }
-
- @Input
- public String getVersion() {
- return mVersion;
- }
-
- public void setVersion(String version) {
- mVersion = version;
- }
-
- @OutputFile
- public File getOutputFile() {
- return mOutputFile;
- }
-
- public void setOutputFile(File outputFile) {
- mOutputFile = outputFile;
- }
-
- /**
- * The main method for actually writing out the file.
- *
- * @throws IOException
- */
- @TaskAction
- public void run() throws IOException {
- PrintWriter writer = new PrintWriter(mOutputFile);
- writer.println(mVersion);
- writer.close();
- }
-}
diff --git a/android/support/animation/AnimationHandler.java b/android/support/animation/AnimationHandler.java
index 6c39b23..24bc43a 100644
--- a/android/support/animation/AnimationHandler.java
+++ b/android/support/animation/AnimationHandler.java
@@ -35,8 +35,6 @@
* The handler uses the Choreographer by default for doing periodic callbacks. A custom
* AnimationFrameCallbackProvider can be set on the handler to provide timing pulse that
* may be independent of UI frame update. This could be useful in testing.
- *
- * @hide
*/
class AnimationHandler {
/**
@@ -57,7 +55,7 @@
* the new frame, so that they can update animation values as needed.
*/
class AnimationCallbackDispatcher {
- public void dispatchAnimationFrame() {
+ void dispatchAnimationFrame() {
mCurrentFrameTime = SystemClock.uptimeMillis();
AnimationHandler.this.doAnimationFrame(mCurrentFrameTime);
if (mAnimationCallbacks.size() > 0) {
@@ -72,7 +70,6 @@
/**
* Internal per-thread collections used to avoid set collisions as animations start and end
* while being processed.
- * @hide
*/
private final SimpleArrayMap<AnimationFrameCallback, Long> mDelayedCallbackStartTime =
new SimpleArrayMap<>();
@@ -249,7 +246,7 @@
* timing pulse without using Choreographer. That way we could use any arbitrary interval for
* our timing pulse in the tests.
*/
- public abstract static class AnimationFrameCallbackProvider {
+ abstract static class AnimationFrameCallbackProvider {
final AnimationCallbackDispatcher mDispatcher;
AnimationFrameCallbackProvider(AnimationCallbackDispatcher dispatcher) {
mDispatcher = dispatcher;
diff --git a/android/support/animation/DynamicAnimation.java b/android/support/animation/DynamicAnimation.java
index 8ea48b9..7cbd5bb 100644
--- a/android/support/animation/DynamicAnimation.java
+++ b/android/support/animation/DynamicAnimation.java
@@ -18,6 +18,7 @@
import android.os.Looper;
import android.support.annotation.FloatRange;
+import android.support.annotation.RestrictTo;
import android.support.v4.view.ViewCompat;
import android.util.AndroidRuntimeException;
import android.view.View;
@@ -631,6 +632,7 @@
*
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
public boolean doAnimationFrame(long frameTime) {
if (mLastFrameTime == 0) {
diff --git a/android/support/animation/SpringForce.java b/android/support/animation/SpringForce.java
index 5f95aa8..dfb4c67 100644
--- a/android/support/animation/SpringForce.java
+++ b/android/support/animation/SpringForce.java
@@ -17,6 +17,7 @@
package android.support.animation;
import android.support.annotation.FloatRange;
+import android.support.annotation.RestrictTo;
/**
* Spring Force defines the characteristics of the spring being used in the animation.
@@ -210,6 +211,7 @@
/**
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
public float getAcceleration(float lastDisplacement, float lastVelocity) {
@@ -224,6 +226,7 @@
/**
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
public boolean isAtEquilibrium(float value, float velocity) {
if (Math.abs(velocity) < mVelocityThreshold
diff --git a/android/support/customtabs/CustomTabsClient.java b/android/support/customtabs/CustomTabsClient.java
index 2e955cb..371b5a1 100644
--- a/android/support/customtabs/CustomTabsClient.java
+++ b/android/support/customtabs/CustomTabsClient.java
@@ -45,7 +45,7 @@
private final ICustomTabsService mService;
private final ComponentName mServiceComponentName;
- /**@hide*/
+ /** @hide */
@RestrictTo(LIBRARY_GROUP)
CustomTabsClient(ICustomTabsService service, ComponentName componentName) {
mService = service;
diff --git a/android/support/design/internal/BaselineLayout.java b/android/support/design/internal/BaselineLayout.java
deleted file mode 100644
index 0bfdf24..0000000
--- a/android/support/design/internal/BaselineLayout.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * 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.support.design.internal;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * A simple ViewGroup that aligns all the views inside on a baseline. Note: bottom padding for this
- * view will be measured starting from the baseline.
- *
- * @hide
- */
-public class BaselineLayout extends ViewGroup {
- private int mBaseline = -1;
-
- public BaselineLayout(Context context) {
- super(context, null, 0);
- }
-
- public BaselineLayout(Context context, AttributeSet attrs) {
- super(context, attrs, 0);
- }
-
- public BaselineLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- final int count = getChildCount();
- int maxWidth = 0;
- int maxHeight = 0;
- int maxChildBaseline = -1;
- int maxChildDescent = -1;
- int childState = 0;
-
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- if (child.getVisibility() == GONE) {
- continue;
- }
-
- measureChild(child, widthMeasureSpec, heightMeasureSpec);
- final int baseline = child.getBaseline();
- if (baseline != -1) {
- maxChildBaseline = Math.max(maxChildBaseline, baseline);
- maxChildDescent = Math.max(maxChildDescent, child.getMeasuredHeight() - baseline);
- }
- maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
- maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
- childState = View.combineMeasuredStates(childState, child.getMeasuredState());
- }
- if (maxChildBaseline != -1) {
- maxChildDescent = Math.max(maxChildDescent, getPaddingBottom());
- maxHeight = Math.max(maxHeight, maxChildBaseline + maxChildDescent);
- mBaseline = maxChildBaseline;
- }
- maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
- maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
- setMeasuredDimension(
- View.resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
- View.resolveSizeAndState(maxHeight, heightMeasureSpec,
- childState << MEASURED_HEIGHT_STATE_SHIFT));
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- final int count = getChildCount();
- final int parentLeft = getPaddingLeft();
- final int parentRight = right - left - getPaddingRight();
- final int parentContentWidth = parentRight - parentLeft;
- final int parentTop = getPaddingTop();
-
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- if (child.getVisibility() == GONE) {
- continue;
- }
-
- final int width = child.getMeasuredWidth();
- final int height = child.getMeasuredHeight();
-
- final int childLeft = parentLeft + (parentContentWidth - width) / 2;
- final int childTop;
- if (mBaseline != -1 && child.getBaseline() != -1) {
- childTop = parentTop + mBaseline - child.getBaseline();
- } else {
- childTop = parentTop;
- }
-
- child.layout(childLeft, childTop, childLeft + width, childTop + height);
- }
- }
-
- @Override
- public int getBaseline() {
- return mBaseline;
- }
-}
diff --git a/android/support/design/internal/BottomNavigationItemView.java b/android/support/design/internal/BottomNavigationItemView.java
deleted file mode 100644
index fe5e636..0000000
--- a/android/support/design/internal/BottomNavigationItemView.java
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
- * 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.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.design.R;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import android.support.v4.view.PointerIconCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.support.v7.view.menu.MenuView;
-import android.support.v7.widget.TooltipCompat;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class BottomNavigationItemView extends FrameLayout implements MenuView.ItemView {
- public static final int INVALID_ITEM_POSITION = -1;
-
- private static final int[] CHECKED_STATE_SET = { android.R.attr.state_checked };
-
- private final int mDefaultMargin;
- private final int mShiftAmount;
- private final float mScaleUpFactor;
- private final float mScaleDownFactor;
-
- private boolean mShiftingMode;
-
- private ImageView mIcon;
- private final TextView mSmallLabel;
- private final TextView mLargeLabel;
- private int mItemPosition = INVALID_ITEM_POSITION;
-
- private MenuItemImpl mItemData;
-
- private ColorStateList mIconTint;
-
- public BottomNavigationItemView(@NonNull Context context) {
- this(context, null);
- }
-
- public BottomNavigationItemView(@NonNull Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public BottomNavigationItemView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- final Resources res = getResources();
- int inactiveLabelSize =
- res.getDimensionPixelSize(R.dimen.design_bottom_navigation_text_size);
- int activeLabelSize = res.getDimensionPixelSize(
- R.dimen.design_bottom_navigation_active_text_size);
- mDefaultMargin = res.getDimensionPixelSize(R.dimen.design_bottom_navigation_margin);
- mShiftAmount = inactiveLabelSize - activeLabelSize;
- mScaleUpFactor = 1f * activeLabelSize / inactiveLabelSize;
- mScaleDownFactor = 1f * inactiveLabelSize / activeLabelSize;
-
- LayoutInflater.from(context).inflate(R.layout.design_bottom_navigation_item, this, true);
- setBackgroundResource(R.drawable.design_bottom_navigation_item_background);
- mIcon = findViewById(R.id.icon);
- mSmallLabel = findViewById(R.id.smallLabel);
- mLargeLabel = findViewById(R.id.largeLabel);
- }
-
- @Override
- public void initialize(MenuItemImpl itemData, int menuType) {
- mItemData = itemData;
- setCheckable(itemData.isCheckable());
- setChecked(itemData.isChecked());
- setEnabled(itemData.isEnabled());
- setIcon(itemData.getIcon());
- setTitle(itemData.getTitle());
- setId(itemData.getItemId());
- setContentDescription(itemData.getContentDescription());
- TooltipCompat.setTooltipText(this, itemData.getTooltipText());
- }
-
- public void setItemPosition(int position) {
- mItemPosition = position;
- }
-
- public int getItemPosition() {
- return mItemPosition;
- }
-
- public void setShiftingMode(boolean enabled) {
- mShiftingMode = enabled;
- }
-
- @Override
- public MenuItemImpl getItemData() {
- return mItemData;
- }
-
- @Override
- public void setTitle(CharSequence title) {
- mSmallLabel.setText(title);
- mLargeLabel.setText(title);
- }
-
- @Override
- public void setCheckable(boolean checkable) {
- refreshDrawableState();
- }
-
- @Override
- public void setChecked(boolean checked) {
- mLargeLabel.setPivotX(mLargeLabel.getWidth() / 2);
- mLargeLabel.setPivotY(mLargeLabel.getBaseline());
- mSmallLabel.setPivotX(mSmallLabel.getWidth() / 2);
- mSmallLabel.setPivotY(mSmallLabel.getBaseline());
- if (mShiftingMode) {
- if (checked) {
- LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
- iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
- iconParams.topMargin = mDefaultMargin;
- mIcon.setLayoutParams(iconParams);
- mLargeLabel.setVisibility(VISIBLE);
- mLargeLabel.setScaleX(1f);
- mLargeLabel.setScaleY(1f);
- } else {
- LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
- iconParams.gravity = Gravity.CENTER;
- iconParams.topMargin = mDefaultMargin;
- mIcon.setLayoutParams(iconParams);
- mLargeLabel.setVisibility(INVISIBLE);
- mLargeLabel.setScaleX(0.5f);
- mLargeLabel.setScaleY(0.5f);
- }
- mSmallLabel.setVisibility(INVISIBLE);
- } else {
- if (checked) {
- LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
- iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
- iconParams.topMargin = mDefaultMargin + mShiftAmount;
- mIcon.setLayoutParams(iconParams);
- mLargeLabel.setVisibility(VISIBLE);
- mSmallLabel.setVisibility(INVISIBLE);
-
- mLargeLabel.setScaleX(1f);
- mLargeLabel.setScaleY(1f);
- mSmallLabel.setScaleX(mScaleUpFactor);
- mSmallLabel.setScaleY(mScaleUpFactor);
- } else {
- LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
- iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
- iconParams.topMargin = mDefaultMargin;
- mIcon.setLayoutParams(iconParams);
- mLargeLabel.setVisibility(INVISIBLE);
- mSmallLabel.setVisibility(VISIBLE);
-
- mLargeLabel.setScaleX(mScaleDownFactor);
- mLargeLabel.setScaleY(mScaleDownFactor);
- mSmallLabel.setScaleX(1f);
- mSmallLabel.setScaleY(1f);
- }
- }
-
- refreshDrawableState();
- }
-
- @Override
- public void setEnabled(boolean enabled) {
- super.setEnabled(enabled);
- mSmallLabel.setEnabled(enabled);
- mLargeLabel.setEnabled(enabled);
- mIcon.setEnabled(enabled);
-
- if (enabled) {
- ViewCompat.setPointerIcon(this,
- PointerIconCompat.getSystemIcon(getContext(), PointerIconCompat.TYPE_HAND));
- } else {
- ViewCompat.setPointerIcon(this, null);
- }
-
- }
-
- @Override
- public int[] onCreateDrawableState(final int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
- if (mItemData != null && mItemData.isCheckable() && mItemData.isChecked()) {
- mergeDrawableStates(drawableState, CHECKED_STATE_SET);
- }
- return drawableState;
- }
-
- @Override
- public void setShortcut(boolean showShortcut, char shortcutKey) {
- }
-
- @Override
- public void setIcon(Drawable icon) {
- if (icon != null) {
- Drawable.ConstantState state = icon.getConstantState();
- icon = DrawableCompat.wrap(state == null ? icon : state.newDrawable()).mutate();
- DrawableCompat.setTintList(icon, mIconTint);
- }
- mIcon.setImageDrawable(icon);
- }
-
- @Override
- public boolean prefersCondensedTitle() {
- return false;
- }
-
- @Override
- public boolean showsIcon() {
- return true;
- }
-
- public void setIconTintList(ColorStateList tint) {
- mIconTint = tint;
- if (mItemData != null) {
- // Update the icon so that the tint takes effect
- setIcon(mItemData.getIcon());
- }
- }
-
- public void setTextColor(ColorStateList color) {
- mSmallLabel.setTextColor(color);
- mLargeLabel.setTextColor(color);
- }
-
- public void setItemBackground(int background) {
- Drawable backgroundDrawable = background == 0
- ? null : ContextCompat.getDrawable(getContext(), background);
- ViewCompat.setBackground(this, backgroundDrawable);
- }
-}
diff --git a/android/support/design/internal/BottomNavigationMenu.java b/android/support/design/internal/BottomNavigationMenu.java
deleted file mode 100644
index a86d2ad..0000000
--- a/android/support/design/internal/BottomNavigationMenu.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.support.annotation.RestrictTo;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.view.MenuItem;
-import android.view.SubMenu;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public final class BottomNavigationMenu extends MenuBuilder {
- public static final int MAX_ITEM_COUNT = 5;
-
- public BottomNavigationMenu(Context context) {
- super(context);
- }
-
- @Override
- public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) {
- throw new UnsupportedOperationException("BottomNavigationView does not support submenus");
- }
-
- @Override
- protected MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) {
- if (size() + 1 > MAX_ITEM_COUNT) {
- throw new IllegalArgumentException(
- "Maximum number of items supported by BottomNavigationView is " + MAX_ITEM_COUNT
- + ". Limit can be checked with BottomNavigationView#getMaxItemCount()");
- }
- stopDispatchingItemsChanged();
- final MenuItem item = super.addInternal(group, id, categoryOrder, title);
- if (item instanceof MenuItemImpl) {
- ((MenuItemImpl) item).setExclusiveCheckable(true);
- }
- startDispatchingItemsChanged();
- return item;
- }
-}
diff --git a/android/support/design/internal/BottomNavigationMenuView.java b/android/support/design/internal/BottomNavigationMenuView.java
deleted file mode 100644
index bf33454..0000000
--- a/android/support/design/internal/BottomNavigationMenuView.java
+++ /dev/null
@@ -1,343 +0,0 @@
-/*
- * 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.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.design.R;
-import android.support.transition.AutoTransition;
-import android.support.transition.TransitionManager;
-import android.support.transition.TransitionSet;
-import android.support.v4.util.Pools;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.animation.FastOutSlowInInterpolator;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.support.v7.view.menu.MenuView;
-import android.util.AttributeSet;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * @hide For internal use only.
- */
-@RestrictTo(LIBRARY_GROUP)
-public class BottomNavigationMenuView extends ViewGroup implements MenuView {
- private static final long ACTIVE_ANIMATION_DURATION_MS = 115L;
-
- private final TransitionSet mSet;
- private final int mInactiveItemMaxWidth;
- private final int mInactiveItemMinWidth;
- private final int mActiveItemMaxWidth;
- private final int mItemHeight;
- private final OnClickListener mOnClickListener;
- private final Pools.Pool<BottomNavigationItemView> mItemPool = new Pools.SynchronizedPool<>(5);
-
- private boolean mShiftingMode = true;
-
- private BottomNavigationItemView[] mButtons;
- private int mSelectedItemId = 0;
- private int mSelectedItemPosition = 0;
- private ColorStateList mItemIconTint;
- private ColorStateList mItemTextColor;
- private int mItemBackgroundRes;
- private int[] mTempChildWidths;
-
- private BottomNavigationPresenter mPresenter;
- private MenuBuilder mMenu;
-
- public BottomNavigationMenuView(Context context) {
- this(context, null);
- }
-
- public BottomNavigationMenuView(Context context, AttributeSet attrs) {
- super(context, attrs);
- final Resources res = getResources();
- mInactiveItemMaxWidth = res.getDimensionPixelSize(
- R.dimen.design_bottom_navigation_item_max_width);
- mInactiveItemMinWidth = res.getDimensionPixelSize(
- R.dimen.design_bottom_navigation_item_min_width);
- mActiveItemMaxWidth = res.getDimensionPixelSize(
- R.dimen.design_bottom_navigation_active_item_max_width);
- mItemHeight = res.getDimensionPixelSize(R.dimen.design_bottom_navigation_height);
-
- mSet = new AutoTransition();
- mSet.setOrdering(TransitionSet.ORDERING_TOGETHER);
- mSet.setDuration(ACTIVE_ANIMATION_DURATION_MS);
- mSet.setInterpolator(new FastOutSlowInInterpolator());
- mSet.addTransition(new TextScale());
-
- mOnClickListener = new OnClickListener() {
- @Override
- public void onClick(View v) {
- final BottomNavigationItemView itemView = (BottomNavigationItemView) v;
- MenuItem item = itemView.getItemData();
- if (!mMenu.performItemAction(item, mPresenter, 0)) {
- item.setChecked(true);
- }
- }
- };
- mTempChildWidths = new int[BottomNavigationMenu.MAX_ITEM_COUNT];
- }
-
- @Override
- public void initialize(MenuBuilder menu) {
- mMenu = menu;
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- final int width = MeasureSpec.getSize(widthMeasureSpec);
- final int count = getChildCount();
-
- final int heightSpec = MeasureSpec.makeMeasureSpec(mItemHeight, MeasureSpec.EXACTLY);
-
- if (mShiftingMode) {
- final int inactiveCount = count - 1;
- final int activeMaxAvailable = width - inactiveCount * mInactiveItemMinWidth;
- final int activeWidth = Math.min(activeMaxAvailable, mActiveItemMaxWidth);
- final int inactiveMaxAvailable = (width - activeWidth) / inactiveCount;
- final int inactiveWidth = Math.min(inactiveMaxAvailable, mInactiveItemMaxWidth);
- int extra = width - activeWidth - inactiveWidth * inactiveCount;
- for (int i = 0; i < count; i++) {
- mTempChildWidths[i] = (i == mSelectedItemPosition) ? activeWidth : inactiveWidth;
- if (extra > 0) {
- mTempChildWidths[i]++;
- extra--;
- }
- }
- } else {
- final int maxAvailable = width / (count == 0 ? 1 : count);
- final int childWidth = Math.min(maxAvailable, mActiveItemMaxWidth);
- int extra = width - childWidth * count;
- for (int i = 0; i < count; i++) {
- mTempChildWidths[i] = childWidth;
- if (extra > 0) {
- mTempChildWidths[i]++;
- extra--;
- }
- }
- }
-
- int totalWidth = 0;
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- if (child.getVisibility() == GONE) {
- continue;
- }
- child.measure(MeasureSpec.makeMeasureSpec(mTempChildWidths[i], MeasureSpec.EXACTLY),
- heightSpec);
- ViewGroup.LayoutParams params = child.getLayoutParams();
- params.width = child.getMeasuredWidth();
- totalWidth += child.getMeasuredWidth();
- }
- setMeasuredDimension(
- View.resolveSizeAndState(totalWidth,
- MeasureSpec.makeMeasureSpec(totalWidth, MeasureSpec.EXACTLY), 0),
- View.resolveSizeAndState(mItemHeight, heightSpec, 0));
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- final int count = getChildCount();
- final int width = right - left;
- final int height = bottom - top;
- int used = 0;
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- if (child.getVisibility() == GONE) {
- continue;
- }
- if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL) {
- child.layout(width - used - child.getMeasuredWidth(), 0, width - used, height);
- } else {
- child.layout(used, 0, child.getMeasuredWidth() + used, height);
- }
- used += child.getMeasuredWidth();
- }
- }
-
- @Override
- public int getWindowAnimations() {
- return 0;
- }
-
- /**
- * Sets the tint which is applied to the menu items' icons.
- *
- * @param tint the tint to apply
- */
- public void setIconTintList(ColorStateList tint) {
- mItemIconTint = tint;
- if (mButtons == null) return;
- for (BottomNavigationItemView item : mButtons) {
- item.setIconTintList(tint);
- }
- }
-
- /**
- * Returns the tint which is applied to menu items' icons.
- *
- * @return the ColorStateList that is used to tint menu items' icons
- */
- @Nullable
- public ColorStateList getIconTintList() {
- return mItemIconTint;
- }
-
- /**
- * Sets the text color to be used on menu items.
- *
- * @param color the ColorStateList used for menu items' text.
- */
- public void setItemTextColor(ColorStateList color) {
- mItemTextColor = color;
- if (mButtons == null) return;
- for (BottomNavigationItemView item : mButtons) {
- item.setTextColor(color);
- }
- }
-
- /**
- * Returns the text color used on menu items.
- *
- * @return the ColorStateList used for menu items' text
- */
- public ColorStateList getItemTextColor() {
- return mItemTextColor;
- }
-
- /**
- * Sets the resource ID to be used for item background.
- *
- * @param background the resource ID of the background
- */
- public void setItemBackgroundRes(int background) {
- mItemBackgroundRes = background;
- if (mButtons == null) return;
- for (BottomNavigationItemView item : mButtons) {
- item.setItemBackground(background);
- }
- }
-
- /**
- * Returns the resource ID for the background of the menu items.
- *
- * @return the resource ID for the background
- */
- public int getItemBackgroundRes() {
- return mItemBackgroundRes;
- }
-
- public void setPresenter(BottomNavigationPresenter presenter) {
- mPresenter = presenter;
- }
-
- public void buildMenuView() {
- removeAllViews();
- if (mButtons != null) {
- for (BottomNavigationItemView item : mButtons) {
- mItemPool.release(item);
- }
- }
- if (mMenu.size() == 0) {
- mSelectedItemId = 0;
- mSelectedItemPosition = 0;
- mButtons = null;
- return;
- }
- mButtons = new BottomNavigationItemView[mMenu.size()];
- mShiftingMode = mMenu.size() > 3;
- for (int i = 0; i < mMenu.size(); i++) {
- mPresenter.setUpdateSuspended(true);
- mMenu.getItem(i).setCheckable(true);
- mPresenter.setUpdateSuspended(false);
- BottomNavigationItemView child = getNewItem();
- mButtons[i] = child;
- child.setIconTintList(mItemIconTint);
- child.setTextColor(mItemTextColor);
- child.setItemBackground(mItemBackgroundRes);
- child.setShiftingMode(mShiftingMode);
- child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
- child.setItemPosition(i);
- child.setOnClickListener(mOnClickListener);
- addView(child);
- }
- mSelectedItemPosition = Math.min(mMenu.size() - 1, mSelectedItemPosition);
- mMenu.getItem(mSelectedItemPosition).setChecked(true);
- }
-
- public void updateMenuView() {
- final int menuSize = mMenu.size();
- if (menuSize != mButtons.length) {
- // The size has changed. Rebuild menu view from scratch.
- buildMenuView();
- return;
- }
- int previousSelectedId = mSelectedItemId;
-
- for (int i = 0; i < menuSize; i++) {
- MenuItem item = mMenu.getItem(i);
- if (item.isChecked()) {
- mSelectedItemId = item.getItemId();
- mSelectedItemPosition = i;
- }
- }
- if (previousSelectedId != mSelectedItemId) {
- // Note: this has to be called before BottomNavigationItemView#initialize().
- TransitionManager.beginDelayedTransition(this, mSet);
- }
-
- for (int i = 0; i < menuSize; i++) {
- mPresenter.setUpdateSuspended(true);
- mButtons[i].initialize((MenuItemImpl) mMenu.getItem(i), 0);
- mPresenter.setUpdateSuspended(false);
- }
-
- }
-
- private BottomNavigationItemView getNewItem() {
- BottomNavigationItemView item = mItemPool.acquire();
- if (item == null) {
- item = new BottomNavigationItemView(getContext());
- }
- return item;
- }
-
- public int getSelectedItemId() {
- return mSelectedItemId;
- }
-
- void tryRestoreSelectedItemId(int itemId) {
- final int size = mMenu.size();
- for (int i = 0; i < size; i++) {
- MenuItem item = mMenu.getItem(i);
- if (itemId == item.getItemId()) {
- mSelectedItemId = itemId;
- mSelectedItemPosition = i;
- item.setChecked(true);
- break;
- }
- }
- }
-}
diff --git a/android/support/design/internal/BottomNavigationPresenter.java b/android/support/design/internal/BottomNavigationPresenter.java
deleted file mode 100644
index 1343a4b..0000000
--- a/android/support/design/internal/BottomNavigationPresenter.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * 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.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.support.v7.view.menu.MenuPresenter;
-import android.support.v7.view.menu.MenuView;
-import android.support.v7.view.menu.SubMenuBuilder;
-import android.view.ViewGroup;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class BottomNavigationPresenter implements MenuPresenter {
- private MenuBuilder mMenu;
- private BottomNavigationMenuView mMenuView;
- private boolean mUpdateSuspended = false;
- private int mId;
-
- public void setBottomNavigationMenuView(BottomNavigationMenuView menuView) {
- mMenuView = menuView;
- }
-
- @Override
- public void initForMenu(Context context, MenuBuilder menu) {
- mMenuView.initialize(mMenu);
- mMenu = menu;
- }
-
- @Override
- public MenuView getMenuView(ViewGroup root) {
- return mMenuView;
- }
-
- @Override
- public void updateMenuView(boolean cleared) {
- if (mUpdateSuspended) return;
- if (cleared) {
- mMenuView.buildMenuView();
- } else {
- mMenuView.updateMenuView();
- }
- }
-
- @Override
- public void setCallback(Callback cb) {}
-
- @Override
- public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
- return false;
- }
-
- @Override
- public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {}
-
- @Override
- public boolean flagActionItems() {
- return false;
- }
-
- @Override
- public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
- return false;
- }
-
- @Override
- public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
- return false;
- }
-
- public void setId(int id) {
- mId = id;
- }
-
- @Override
- public int getId() {
- return mId;
- }
-
- @Override
- public Parcelable onSaveInstanceState() {
- SavedState savedState = new SavedState();
- savedState.selectedItemId = mMenuView.getSelectedItemId();
- return savedState;
- }
-
- @Override
- public void onRestoreInstanceState(Parcelable state) {
- if (state instanceof SavedState) {
- mMenuView.tryRestoreSelectedItemId(((SavedState) state).selectedItemId);
- }
- }
-
- public void setUpdateSuspended(boolean updateSuspended) {
- mUpdateSuspended = updateSuspended;
- }
-
- static class SavedState implements Parcelable {
- int selectedItemId;
-
- SavedState() {}
-
- SavedState(Parcel in) {
- selectedItemId = in.readInt();
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(@NonNull Parcel out, int flags) {
- out.writeInt(selectedItemId);
- }
-
- public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
-}
diff --git a/android/support/design/internal/ForegroundLinearLayout.java b/android/support/design/internal/ForegroundLinearLayout.java
deleted file mode 100644
index 6d90503..0000000
--- a/android/support/design/internal/ForegroundLinearLayout.java
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.NonNull;
-import android.support.annotation.RequiresApi;
-import android.support.annotation.RestrictTo;
-import android.support.design.R;
-import android.support.v7.widget.LinearLayoutCompat;
-import android.util.AttributeSet;
-import android.view.Gravity;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class ForegroundLinearLayout extends LinearLayoutCompat {
-
- private Drawable mForeground;
-
- private final Rect mSelfBounds = new Rect();
-
- private final Rect mOverlayBounds = new Rect();
-
- private int mForegroundGravity = Gravity.FILL;
-
- protected boolean mForegroundInPadding = true;
-
- boolean mForegroundBoundsChanged = false;
-
- public ForegroundLinearLayout(Context context) {
- this(context, null);
- }
-
- public ForegroundLinearLayout(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public ForegroundLinearLayout(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ForegroundLinearLayout,
- defStyle, 0);
-
- mForegroundGravity = a.getInt(
- R.styleable.ForegroundLinearLayout_android_foregroundGravity, mForegroundGravity);
-
- final Drawable d = a.getDrawable(R.styleable.ForegroundLinearLayout_android_foreground);
- if (d != null) {
- setForeground(d);
- }
-
- mForegroundInPadding = a.getBoolean(
- R.styleable.ForegroundLinearLayout_foregroundInsidePadding, true);
-
- a.recycle();
- }
-
- /**
- * Describes how the foreground is positioned.
- *
- * @return foreground gravity.
- * @see #setForegroundGravity(int)
- */
- @Override
- public int getForegroundGravity() {
- return mForegroundGravity;
- }
-
- /**
- * Describes how the foreground is positioned. Defaults to START and TOP.
- *
- * @param foregroundGravity See {@link android.view.Gravity}
- * @see #getForegroundGravity()
- */
- @Override
- public void setForegroundGravity(int foregroundGravity) {
- if (mForegroundGravity != foregroundGravity) {
- if ((foregroundGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
- foregroundGravity |= Gravity.START;
- }
-
- if ((foregroundGravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
- foregroundGravity |= Gravity.TOP;
- }
-
- mForegroundGravity = foregroundGravity;
-
- if (mForegroundGravity == Gravity.FILL && mForeground != null) {
- Rect padding = new Rect();
- mForeground.getPadding(padding);
- }
-
- requestLayout();
- }
- }
-
- @Override
- protected boolean verifyDrawable(Drawable who) {
- return super.verifyDrawable(who) || (who == mForeground);
- }
-
- @RequiresApi(11)
- @Override
- public void jumpDrawablesToCurrentState() {
- super.jumpDrawablesToCurrentState();
- if (mForeground != null) {
- mForeground.jumpToCurrentState();
- }
- }
-
- @Override
- protected void drawableStateChanged() {
- super.drawableStateChanged();
- if (mForeground != null && mForeground.isStateful()) {
- mForeground.setState(getDrawableState());
- }
- }
-
- /**
- * Supply a Drawable that is to be rendered on top of all of the child
- * views in the frame layout. Any padding in the Drawable will be taken
- * into account by ensuring that the children are inset to be placed
- * inside of the padding area.
- *
- * @param drawable The Drawable to be drawn on top of the children.
- */
- @Override
- public void setForeground(Drawable drawable) {
- if (mForeground != drawable) {
- if (mForeground != null) {
- mForeground.setCallback(null);
- unscheduleDrawable(mForeground);
- }
-
- mForeground = drawable;
-
- if (drawable != null) {
- setWillNotDraw(false);
- drawable.setCallback(this);
- if (drawable.isStateful()) {
- drawable.setState(getDrawableState());
- }
- if (mForegroundGravity == Gravity.FILL) {
- Rect padding = new Rect();
- drawable.getPadding(padding);
- }
- } else {
- setWillNotDraw(true);
- }
- requestLayout();
- invalidate();
- }
- }
-
- /**
- * Returns the drawable used as the foreground of this FrameLayout. The
- * foreground drawable, if non-null, is always drawn on top of the children.
- *
- * @return A Drawable or null if no foreground was set.
- */
- @Override
- public Drawable getForeground() {
- return mForeground;
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- mForegroundBoundsChanged |= changed;
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- mForegroundBoundsChanged = true;
- }
-
- @Override
- public void draw(@NonNull Canvas canvas) {
- super.draw(canvas);
-
- if (mForeground != null) {
- final Drawable foreground = mForeground;
-
- if (mForegroundBoundsChanged) {
- mForegroundBoundsChanged = false;
- final Rect selfBounds = mSelfBounds;
- final Rect overlayBounds = mOverlayBounds;
-
- final int w = getRight() - getLeft();
- final int h = getBottom() - getTop();
-
- if (mForegroundInPadding) {
- selfBounds.set(0, 0, w, h);
- } else {
- selfBounds.set(getPaddingLeft(), getPaddingTop(),
- w - getPaddingRight(), h - getPaddingBottom());
- }
-
- Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(),
- foreground.getIntrinsicHeight(), selfBounds, overlayBounds);
- foreground.setBounds(overlayBounds);
- }
-
- foreground.draw(canvas);
- }
- }
-
- @RequiresApi(21)
- @Override
- public void drawableHotspotChanged(float x, float y) {
- super.drawableHotspotChanged(x, y);
- if (mForeground != null) {
- mForeground.setHotspot(x, y);
- }
- }
-
-}
diff --git a/android/support/design/internal/NavigationMenu.java b/android/support/design/internal/NavigationMenu.java
deleted file mode 100644
index a0ec5e0..0000000
--- a/android/support/design/internal/NavigationMenu.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.support.annotation.RestrictTo;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.support.v7.view.menu.SubMenuBuilder;
-import android.view.SubMenu;
-
-/**
- * This is a {@link MenuBuilder} that returns an instance of {@link NavigationSubMenu} instead of
- * {@link SubMenuBuilder} when a sub menu is created.
- *
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class NavigationMenu extends MenuBuilder {
-
- public NavigationMenu(Context context) {
- super(context);
- }
-
- @Override
- public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) {
- final MenuItemImpl item = (MenuItemImpl) addInternal(group, id, categoryOrder, title);
- final SubMenuBuilder subMenu = new NavigationSubMenu(getContext(), this, item);
- item.setSubMenu(subMenu);
- return subMenu;
- }
-
-}
diff --git a/android/support/design/internal/NavigationMenuItemView.java b/android/support/design/internal/NavigationMenuItemView.java
deleted file mode 100644
index eea9e90..0000000
--- a/android/support/design/internal/NavigationMenuItemView.java
+++ /dev/null
@@ -1,272 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.StateListDrawable;
-import android.support.annotation.RestrictTo;
-import android.support.design.R;
-import android.support.v4.content.res.ResourcesCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import android.support.v4.view.AccessibilityDelegateCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.accessibility.AccessibilityEventCompat;
-import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
-import android.support.v4.widget.TextViewCompat;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.support.v7.view.menu.MenuView;
-import android.support.v7.widget.TooltipCompat;
-import android.util.AttributeSet;
-import android.util.TypedValue;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewStub;
-import android.widget.CheckedTextView;
-import android.widget.FrameLayout;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class NavigationMenuItemView extends ForegroundLinearLayout implements MenuView.ItemView {
-
- private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
-
- private final int mIconSize;
-
- private boolean mNeedsEmptyIcon;
-
- boolean mCheckable;
-
- private final CheckedTextView mTextView;
-
- private FrameLayout mActionArea;
-
- private MenuItemImpl mItemData;
-
- private ColorStateList mIconTintList;
-
- private boolean mHasIconTintList;
-
- private Drawable mEmptyDrawable;
-
- private final AccessibilityDelegateCompat mAccessibilityDelegate
- = new AccessibilityDelegateCompat() {
-
- @Override
- public void onInitializeAccessibilityNodeInfo(View host,
- AccessibilityNodeInfoCompat info) {
- super.onInitializeAccessibilityNodeInfo(host, info);
- info.setCheckable(mCheckable);
- }
-
- };
-
- public NavigationMenuItemView(Context context) {
- this(context, null);
- }
-
- public NavigationMenuItemView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public NavigationMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- setOrientation(HORIZONTAL);
- LayoutInflater.from(context).inflate(R.layout.design_navigation_menu_item, this, true);
- mIconSize = context.getResources().getDimensionPixelSize(
- R.dimen.design_navigation_icon_size);
- mTextView = findViewById(R.id.design_menu_item_text);
- mTextView.setDuplicateParentStateEnabled(true);
- ViewCompat.setAccessibilityDelegate(mTextView, mAccessibilityDelegate);
- }
-
- @Override
- public void initialize(MenuItemImpl itemData, int menuType) {
- mItemData = itemData;
-
- setVisibility(itemData.isVisible() ? VISIBLE : GONE);
-
- if (getBackground() == null) {
- ViewCompat.setBackground(this, createDefaultBackground());
- }
-
- setCheckable(itemData.isCheckable());
- setChecked(itemData.isChecked());
- setEnabled(itemData.isEnabled());
- setTitle(itemData.getTitle());
- setIcon(itemData.getIcon());
- setActionView(itemData.getActionView());
- setContentDescription(itemData.getContentDescription());
- TooltipCompat.setTooltipText(this, itemData.getTooltipText());
- adjustAppearance();
- }
-
- private boolean shouldExpandActionArea() {
- return mItemData.getTitle() == null &&
- mItemData.getIcon() == null &&
- mItemData.getActionView() != null;
- }
-
- private void adjustAppearance() {
- if (shouldExpandActionArea()) {
- // Expand the actionView area
- mTextView.setVisibility(View.GONE);
- if (mActionArea != null) {
- LayoutParams params = (LayoutParams) mActionArea.getLayoutParams();
- params.width = LayoutParams.MATCH_PARENT;
- mActionArea.setLayoutParams(params);
- }
- } else {
- mTextView.setVisibility(View.VISIBLE);
- if (mActionArea != null) {
- LayoutParams params = (LayoutParams) mActionArea.getLayoutParams();
- params.width = LayoutParams.WRAP_CONTENT;
- mActionArea.setLayoutParams(params);
- }
- }
- }
-
- public void recycle() {
- if (mActionArea != null) {
- mActionArea.removeAllViews();
- }
- mTextView.setCompoundDrawables(null, null, null, null);
- }
-
- private void setActionView(View actionView) {
- if (actionView != null) {
- if (mActionArea == null) {
- mActionArea = (FrameLayout) ((ViewStub) findViewById(
- R.id.design_menu_item_action_area_stub)).inflate();
- }
- mActionArea.removeAllViews();
- mActionArea.addView(actionView);
- }
- }
-
- private StateListDrawable createDefaultBackground() {
- TypedValue value = new TypedValue();
- if (getContext().getTheme().resolveAttribute(
- android.support.v7.appcompat.R.attr.colorControlHighlight, value, true)) {
- StateListDrawable drawable = new StateListDrawable();
- drawable.addState(CHECKED_STATE_SET, new ColorDrawable(value.data));
- drawable.addState(EMPTY_STATE_SET, new ColorDrawable(Color.TRANSPARENT));
- return drawable;
- }
- return null;
- }
-
- @Override
- public MenuItemImpl getItemData() {
- return mItemData;
- }
-
- @Override
- public void setTitle(CharSequence title) {
- mTextView.setText(title);
- }
-
- @Override
- public void setCheckable(boolean checkable) {
- refreshDrawableState();
- if (mCheckable != checkable) {
- mCheckable = checkable;
- mAccessibilityDelegate.sendAccessibilityEvent(mTextView,
- AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED);
- }
- }
-
- @Override
- public void setChecked(boolean checked) {
- refreshDrawableState();
- mTextView.setChecked(checked);
- }
-
- @Override
- public void setShortcut(boolean showShortcut, char shortcutKey) {
- }
-
- @Override
- public void setIcon(Drawable icon) {
- if (icon != null) {
- if (mHasIconTintList) {
- Drawable.ConstantState state = icon.getConstantState();
- icon = DrawableCompat.wrap(state == null ? icon : state.newDrawable()).mutate();
- DrawableCompat.setTintList(icon, mIconTintList);
- }
- icon.setBounds(0, 0, mIconSize, mIconSize);
- } else if (mNeedsEmptyIcon) {
- if (mEmptyDrawable == null) {
- mEmptyDrawable = ResourcesCompat.getDrawable(getResources(),
- R.drawable.navigation_empty_icon, getContext().getTheme());
- if (mEmptyDrawable != null) {
- mEmptyDrawable.setBounds(0, 0, mIconSize, mIconSize);
- }
- }
- icon = mEmptyDrawable;
- }
- TextViewCompat.setCompoundDrawablesRelative(mTextView, icon, null, null, null);
- }
-
- @Override
- public boolean prefersCondensedTitle() {
- return false;
- }
-
- @Override
- public boolean showsIcon() {
- return true;
- }
-
- @Override
- protected int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
- if (mItemData != null && mItemData.isCheckable() && mItemData.isChecked()) {
- mergeDrawableStates(drawableState, CHECKED_STATE_SET);
- }
- return drawableState;
- }
-
- void setIconTintList(ColorStateList tintList) {
- mIconTintList = tintList;
- mHasIconTintList = mIconTintList != null;
- if (mItemData != null) {
- // Update the icon so that the tint takes effect
- setIcon(mItemData.getIcon());
- }
- }
-
- public void setTextAppearance(int textAppearance) {
- TextViewCompat.setTextAppearance(mTextView, textAppearance);
- }
-
- public void setTextColor(ColorStateList colors) {
- mTextView.setTextColor(colors);
- }
-
- public void setNeedsEmptyIcon(boolean needsEmptyIcon) {
- mNeedsEmptyIcon = needsEmptyIcon;
- }
-
-}
diff --git a/android/support/design/internal/NavigationMenuPresenter.java b/android/support/design/internal/NavigationMenuPresenter.java
deleted file mode 100644
index 98ad468..0000000
--- a/android/support/design/internal/NavigationMenuPresenter.java
+++ /dev/null
@@ -1,686 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.support.annotation.LayoutRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.StyleRes;
-import android.support.design.R;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.WindowInsetsCompat;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.support.v7.view.menu.MenuPresenter;
-import android.support.v7.view.menu.MenuView;
-import android.support.v7.view.menu.SubMenuBuilder;
-import android.support.v7.widget.RecyclerView;
-import android.util.SparseArray;
-import android.view.LayoutInflater;
-import android.view.SubMenu;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class NavigationMenuPresenter implements MenuPresenter {
-
- private static final String STATE_HIERARCHY = "android:menu:list";
- private static final String STATE_ADAPTER = "android:menu:adapter";
- private static final String STATE_HEADER = "android:menu:header";
-
- private NavigationMenuView mMenuView;
- LinearLayout mHeaderLayout;
-
- private Callback mCallback;
- MenuBuilder mMenu;
- private int mId;
-
- NavigationMenuAdapter mAdapter;
- LayoutInflater mLayoutInflater;
-
- int mTextAppearance;
- boolean mTextAppearanceSet;
- ColorStateList mTextColor;
- ColorStateList mIconTintList;
- Drawable mItemBackground;
-
- /**
- * Padding to be inserted at the top of the list to avoid the first menu item
- * from being placed underneath the status bar.
- */
- private int mPaddingTopDefault;
-
- /**
- * Padding for separators between items
- */
- int mPaddingSeparator;
-
- @Override
- public void initForMenu(Context context, MenuBuilder menu) {
- mLayoutInflater = LayoutInflater.from(context);
- mMenu = menu;
- Resources res = context.getResources();
- mPaddingSeparator = res.getDimensionPixelOffset(
- R.dimen.design_navigation_separator_vertical_padding);
- }
-
- @Override
- public MenuView getMenuView(ViewGroup root) {
- if (mMenuView == null) {
- mMenuView = (NavigationMenuView) mLayoutInflater.inflate(
- R.layout.design_navigation_menu, root, false);
- if (mAdapter == null) {
- mAdapter = new NavigationMenuAdapter();
- }
- mHeaderLayout = (LinearLayout) mLayoutInflater
- .inflate(R.layout.design_navigation_item_header,
- mMenuView, false);
- mMenuView.setAdapter(mAdapter);
- }
- return mMenuView;
- }
-
- @Override
- public void updateMenuView(boolean cleared) {
- if (mAdapter != null) {
- mAdapter.update();
- }
- }
-
- @Override
- public void setCallback(Callback cb) {
- mCallback = cb;
- }
-
- @Override
- public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
- return false;
- }
-
- @Override
- public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
- if (mCallback != null) {
- mCallback.onCloseMenu(menu, allMenusAreClosing);
- }
- }
-
- @Override
- public boolean flagActionItems() {
- return false;
- }
-
- @Override
- public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
- return false;
- }
-
- @Override
- public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
- return false;
- }
-
- @Override
- public int getId() {
- return mId;
- }
-
- public void setId(int id) {
- mId = id;
- }
-
- @Override
- public Parcelable onSaveInstanceState() {
- if (Build.VERSION.SDK_INT >= 11) {
- // API 9-10 does not support ClassLoaderCreator, therefore things can crash if they're
- // loaded via different loaders. Rather than crash we just won't save state on those
- // platforms
- final Bundle state = new Bundle();
- if (mMenuView != null) {
- SparseArray<Parcelable> hierarchy = new SparseArray<>();
- mMenuView.saveHierarchyState(hierarchy);
- state.putSparseParcelableArray(STATE_HIERARCHY, hierarchy);
- }
- if (mAdapter != null) {
- state.putBundle(STATE_ADAPTER, mAdapter.createInstanceState());
- }
- if (mHeaderLayout != null) {
- SparseArray<Parcelable> header = new SparseArray<>();
- mHeaderLayout.saveHierarchyState(header);
- state.putSparseParcelableArray(STATE_HEADER, header);
- }
- return state;
- }
- return null;
- }
-
- @Override
- public void onRestoreInstanceState(final Parcelable parcelable) {
- if (parcelable instanceof Bundle) {
- Bundle state = (Bundle) parcelable;
- SparseArray<Parcelable> hierarchy = state.getSparseParcelableArray(STATE_HIERARCHY);
- if (hierarchy != null) {
- mMenuView.restoreHierarchyState(hierarchy);
- }
- Bundle adapterState = state.getBundle(STATE_ADAPTER);
- if (adapterState != null) {
- mAdapter.restoreInstanceState(adapterState);
- }
- SparseArray<Parcelable> header = state.getSparseParcelableArray(STATE_HEADER);
- if (header != null) {
- mHeaderLayout.restoreHierarchyState(header);
- }
- }
- }
-
- public void setCheckedItem(MenuItemImpl item) {
- mAdapter.setCheckedItem(item);
- }
-
- public View inflateHeaderView(@LayoutRes int res) {
- View view = mLayoutInflater.inflate(res, mHeaderLayout, false);
- addHeaderView(view);
- return view;
- }
-
- public void addHeaderView(@NonNull View view) {
- mHeaderLayout.addView(view);
- // The padding on top should be cleared.
- mMenuView.setPadding(0, 0, 0, mMenuView.getPaddingBottom());
- }
-
- public void removeHeaderView(@NonNull View view) {
- mHeaderLayout.removeView(view);
- if (mHeaderLayout.getChildCount() == 0) {
- mMenuView.setPadding(0, mPaddingTopDefault, 0, mMenuView.getPaddingBottom());
- }
- }
-
- public int getHeaderCount() {
- return mHeaderLayout.getChildCount();
- }
-
- public View getHeaderView(int index) {
- return mHeaderLayout.getChildAt(index);
- }
-
- @Nullable
- public ColorStateList getItemTintList() {
- return mIconTintList;
- }
-
- public void setItemIconTintList(@Nullable ColorStateList tint) {
- mIconTintList = tint;
- updateMenuView(false);
- }
-
- @Nullable
- public ColorStateList getItemTextColor() {
- return mTextColor;
- }
-
- public void setItemTextColor(@Nullable ColorStateList textColor) {
- mTextColor = textColor;
- updateMenuView(false);
- }
-
- public void setItemTextAppearance(@StyleRes int resId) {
- mTextAppearance = resId;
- mTextAppearanceSet = true;
- updateMenuView(false);
- }
-
- @Nullable
- public Drawable getItemBackground() {
- return mItemBackground;
- }
-
- public void setItemBackground(@Nullable Drawable itemBackground) {
- mItemBackground = itemBackground;
- updateMenuView(false);
- }
-
- public void setUpdateSuspended(boolean updateSuspended) {
- if (mAdapter != null) {
- mAdapter.setUpdateSuspended(updateSuspended);
- }
- }
-
- public void dispatchApplyWindowInsets(WindowInsetsCompat insets) {
- int top = insets.getSystemWindowInsetTop();
- if (mPaddingTopDefault != top) {
- mPaddingTopDefault = top;
- if (mHeaderLayout.getChildCount() == 0) {
- mMenuView.setPadding(0, mPaddingTopDefault, 0, mMenuView.getPaddingBottom());
- }
- }
- ViewCompat.dispatchApplyWindowInsets(mHeaderLayout, insets);
- }
-
- private abstract static class ViewHolder extends RecyclerView.ViewHolder {
-
- public ViewHolder(View itemView) {
- super(itemView);
- }
-
- }
-
- private static class NormalViewHolder extends ViewHolder {
-
- public NormalViewHolder(LayoutInflater inflater, ViewGroup parent,
- View.OnClickListener listener) {
- super(inflater.inflate(R.layout.design_navigation_item, parent, false));
- itemView.setOnClickListener(listener);
- }
-
- }
-
- private static class SubheaderViewHolder extends ViewHolder {
-
- public SubheaderViewHolder(LayoutInflater inflater, ViewGroup parent) {
- super(inflater.inflate(R.layout.design_navigation_item_subheader, parent, false));
- }
-
- }
-
- private static class SeparatorViewHolder extends ViewHolder {
-
- public SeparatorViewHolder(LayoutInflater inflater, ViewGroup parent) {
- super(inflater.inflate(R.layout.design_navigation_item_separator, parent, false));
- }
-
- }
-
- private static class HeaderViewHolder extends ViewHolder {
-
- public HeaderViewHolder(View itemView) {
- super(itemView);
- }
-
- }
-
- /**
- * Handles click events for the menu items. The items has to be {@link NavigationMenuItemView}.
- */
- final View.OnClickListener mOnClickListener = new View.OnClickListener() {
-
- @Override
- public void onClick(View v) {
- NavigationMenuItemView itemView = (NavigationMenuItemView) v;
- setUpdateSuspended(true);
- MenuItemImpl item = itemView.getItemData();
- boolean result = mMenu.performItemAction(item, NavigationMenuPresenter.this, 0);
- if (item != null && item.isCheckable() && result) {
- mAdapter.setCheckedItem(item);
- }
- setUpdateSuspended(false);
- updateMenuView(false);
- }
-
- };
-
- private class NavigationMenuAdapter extends RecyclerView.Adapter<ViewHolder> {
-
- private static final String STATE_CHECKED_ITEM = "android:menu:checked";
-
- private static final String STATE_ACTION_VIEWS = "android:menu:action_views";
- private static final int VIEW_TYPE_NORMAL = 0;
- private static final int VIEW_TYPE_SUBHEADER = 1;
- private static final int VIEW_TYPE_SEPARATOR = 2;
- private static final int VIEW_TYPE_HEADER = 3;
-
- private final ArrayList<NavigationMenuItem> mItems = new ArrayList<>();
- private MenuItemImpl mCheckedItem;
- private boolean mUpdateSuspended;
-
- NavigationMenuAdapter() {
- prepareMenuItems();
- }
-
- @Override
- public long getItemId(int position) {
- return position;
- }
-
- @Override
- public int getItemCount() {
- return mItems.size();
- }
-
- @Override
- public int getItemViewType(int position) {
- NavigationMenuItem item = mItems.get(position);
- if (item instanceof NavigationMenuSeparatorItem) {
- return VIEW_TYPE_SEPARATOR;
- } else if (item instanceof NavigationMenuHeaderItem) {
- return VIEW_TYPE_HEADER;
- } else if (item instanceof NavigationMenuTextItem) {
- NavigationMenuTextItem textItem = (NavigationMenuTextItem) item;
- if (textItem.getMenuItem().hasSubMenu()) {
- return VIEW_TYPE_SUBHEADER;
- } else {
- return VIEW_TYPE_NORMAL;
- }
- }
- throw new RuntimeException("Unknown item type.");
- }
-
- @Override
- public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- switch (viewType) {
- case VIEW_TYPE_NORMAL:
- return new NormalViewHolder(mLayoutInflater, parent, mOnClickListener);
- case VIEW_TYPE_SUBHEADER:
- return new SubheaderViewHolder(mLayoutInflater, parent);
- case VIEW_TYPE_SEPARATOR:
- return new SeparatorViewHolder(mLayoutInflater, parent);
- case VIEW_TYPE_HEADER:
- return new HeaderViewHolder(mHeaderLayout);
- }
- return null;
- }
-
- @Override
- public void onBindViewHolder(ViewHolder holder, int position) {
- switch (getItemViewType(position)) {
- case VIEW_TYPE_NORMAL: {
- NavigationMenuItemView itemView = (NavigationMenuItemView) holder.itemView;
- itemView.setIconTintList(mIconTintList);
- if (mTextAppearanceSet) {
- itemView.setTextAppearance(mTextAppearance);
- }
- if (mTextColor != null) {
- itemView.setTextColor(mTextColor);
- }
- ViewCompat.setBackground(itemView, mItemBackground != null ?
- mItemBackground.getConstantState().newDrawable() : null);
- NavigationMenuTextItem item = (NavigationMenuTextItem) mItems.get(position);
- itemView.setNeedsEmptyIcon(item.needsEmptyIcon);
- itemView.initialize(item.getMenuItem(), 0);
- break;
- }
- case VIEW_TYPE_SUBHEADER: {
- TextView subHeader = (TextView) holder.itemView;
- NavigationMenuTextItem item = (NavigationMenuTextItem) mItems.get(position);
- subHeader.setText(item.getMenuItem().getTitle());
- break;
- }
- case VIEW_TYPE_SEPARATOR: {
- NavigationMenuSeparatorItem item =
- (NavigationMenuSeparatorItem) mItems.get(position);
- holder.itemView.setPadding(0, item.getPaddingTop(), 0,
- item.getPaddingBottom());
- break;
- }
- case VIEW_TYPE_HEADER: {
- break;
- }
- }
-
- }
-
- @Override
- public void onViewRecycled(ViewHolder holder) {
- if (holder instanceof NormalViewHolder) {
- ((NavigationMenuItemView) holder.itemView).recycle();
- }
- }
-
- public void update() {
- prepareMenuItems();
- notifyDataSetChanged();
- }
-
- /**
- * Flattens the visible menu items of {@link #mMenu} into {@link #mItems},
- * while inserting separators between items when necessary.
- */
- private void prepareMenuItems() {
- if (mUpdateSuspended) {
- return;
- }
- mUpdateSuspended = true;
- mItems.clear();
- mItems.add(new NavigationMenuHeaderItem());
-
- int currentGroupId = -1;
- int currentGroupStart = 0;
- boolean currentGroupHasIcon = false;
- for (int i = 0, totalSize = mMenu.getVisibleItems().size(); i < totalSize; i++) {
- MenuItemImpl item = mMenu.getVisibleItems().get(i);
- if (item.isChecked()) {
- setCheckedItem(item);
- }
- if (item.isCheckable()) {
- item.setExclusiveCheckable(false);
- }
- if (item.hasSubMenu()) {
- SubMenu subMenu = item.getSubMenu();
- if (subMenu.hasVisibleItems()) {
- if (i != 0) {
- mItems.add(new NavigationMenuSeparatorItem(mPaddingSeparator, 0));
- }
- mItems.add(new NavigationMenuTextItem(item));
- boolean subMenuHasIcon = false;
- int subMenuStart = mItems.size();
- for (int j = 0, size = subMenu.size(); j < size; j++) {
- MenuItemImpl subMenuItem = (MenuItemImpl) subMenu.getItem(j);
- if (subMenuItem.isVisible()) {
- if (!subMenuHasIcon && subMenuItem.getIcon() != null) {
- subMenuHasIcon = true;
- }
- if (subMenuItem.isCheckable()) {
- subMenuItem.setExclusiveCheckable(false);
- }
- if (item.isChecked()) {
- setCheckedItem(item);
- }
- mItems.add(new NavigationMenuTextItem(subMenuItem));
- }
- }
- if (subMenuHasIcon) {
- appendTransparentIconIfMissing(subMenuStart, mItems.size());
- }
- }
- } else {
- int groupId = item.getGroupId();
- if (groupId != currentGroupId) { // first item in group
- currentGroupStart = mItems.size();
- currentGroupHasIcon = item.getIcon() != null;
- if (i != 0) {
- currentGroupStart++;
- mItems.add(new NavigationMenuSeparatorItem(
- mPaddingSeparator, mPaddingSeparator));
- }
- } else if (!currentGroupHasIcon && item.getIcon() != null) {
- currentGroupHasIcon = true;
- appendTransparentIconIfMissing(currentGroupStart, mItems.size());
- }
- NavigationMenuTextItem textItem = new NavigationMenuTextItem(item);
- textItem.needsEmptyIcon = currentGroupHasIcon;
- mItems.add(textItem);
- currentGroupId = groupId;
- }
- }
- mUpdateSuspended = false;
- }
-
- private void appendTransparentIconIfMissing(int startIndex, int endIndex) {
- for (int i = startIndex; i < endIndex; i++) {
- NavigationMenuTextItem textItem = (NavigationMenuTextItem) mItems.get(i);
- textItem.needsEmptyIcon = true;
- }
- }
-
- public void setCheckedItem(MenuItemImpl checkedItem) {
- if (mCheckedItem == checkedItem || !checkedItem.isCheckable()) {
- return;
- }
- if (mCheckedItem != null) {
- mCheckedItem.setChecked(false);
- }
- mCheckedItem = checkedItem;
- checkedItem.setChecked(true);
- }
-
- public Bundle createInstanceState() {
- Bundle state = new Bundle();
- if (mCheckedItem != null) {
- state.putInt(STATE_CHECKED_ITEM, mCheckedItem.getItemId());
- }
- // Store the states of the action views.
- SparseArray<ParcelableSparseArray> actionViewStates = new SparseArray<>();
- for (int i = 0, size = mItems.size(); i < size; i++) {
- NavigationMenuItem navigationMenuItem = mItems.get(i);
- if (navigationMenuItem instanceof NavigationMenuTextItem) {
- MenuItemImpl item = ((NavigationMenuTextItem) navigationMenuItem).getMenuItem();
- View actionView = item != null ? item.getActionView() : null;
- if (actionView != null) {
- ParcelableSparseArray container = new ParcelableSparseArray();
- actionView.saveHierarchyState(container);
- actionViewStates.put(item.getItemId(), container);
- }
- }
- }
- state.putSparseParcelableArray(STATE_ACTION_VIEWS, actionViewStates);
- return state;
- }
-
- public void restoreInstanceState(Bundle state) {
- int checkedItem = state.getInt(STATE_CHECKED_ITEM, 0);
- if (checkedItem != 0) {
- mUpdateSuspended = true;
- for (int i = 0, size = mItems.size(); i < size; i++) {
- NavigationMenuItem item = mItems.get(i);
- if (item instanceof NavigationMenuTextItem) {
- MenuItemImpl menuItem = ((NavigationMenuTextItem) item).getMenuItem();
- if (menuItem != null && menuItem.getItemId() == checkedItem) {
- setCheckedItem(menuItem);
- break;
- }
- }
- }
- mUpdateSuspended = false;
- prepareMenuItems();
- }
- // Restore the states of the action views.
- SparseArray<ParcelableSparseArray> actionViewStates = state
- .getSparseParcelableArray(STATE_ACTION_VIEWS);
- if (actionViewStates != null) {
- for (int i = 0, size = mItems.size(); i < size; i++) {
- NavigationMenuItem navigationMenuItem = mItems.get(i);
- if (!(navigationMenuItem instanceof NavigationMenuTextItem)) {
- continue;
- }
- MenuItemImpl item = ((NavigationMenuTextItem) navigationMenuItem).getMenuItem();
- if (item == null) {
- continue;
- }
- View actionView = item.getActionView();
- if (actionView == null) {
- continue;
- }
- ParcelableSparseArray container = actionViewStates.get(item.getItemId());
- if (container == null) {
- continue;
- }
- actionView.restoreHierarchyState(container);
- }
- }
- }
-
- public void setUpdateSuspended(boolean updateSuspended) {
- mUpdateSuspended = updateSuspended;
- }
-
- }
-
- /**
- * Unified data model for all sorts of navigation menu items.
- */
- private interface NavigationMenuItem {
- }
-
- /**
- * Normal or subheader items.
- */
- private static class NavigationMenuTextItem implements NavigationMenuItem {
-
- private final MenuItemImpl mMenuItem;
-
- boolean needsEmptyIcon;
-
- NavigationMenuTextItem(MenuItemImpl item) {
- mMenuItem = item;
- }
-
- public MenuItemImpl getMenuItem() {
- return mMenuItem;
- }
-
- }
-
- /**
- * Separator items.
- */
- private static class NavigationMenuSeparatorItem implements NavigationMenuItem {
-
- private final int mPaddingTop;
-
- private final int mPaddingBottom;
-
- public NavigationMenuSeparatorItem(int paddingTop, int paddingBottom) {
- mPaddingTop = paddingTop;
- mPaddingBottom = paddingBottom;
- }
-
- public int getPaddingTop() {
- return mPaddingTop;
- }
-
- public int getPaddingBottom() {
- return mPaddingBottom;
- }
-
- }
-
- /**
- * Header (not subheader) items.
- */
- private static class NavigationMenuHeaderItem implements NavigationMenuItem {
- NavigationMenuHeaderItem() {
- }
- // The actual content is hold by NavigationMenuPresenter#mHeaderLayout.
- }
-
-}
diff --git a/android/support/design/internal/NavigationMenuView.java b/android/support/design/internal/NavigationMenuView.java
deleted file mode 100644
index 711f71e..0000000
--- a/android/support/design/internal/NavigationMenuView.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.support.annotation.RestrictTo;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.view.menu.MenuView;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.util.AttributeSet;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class NavigationMenuView extends RecyclerView implements MenuView {
-
- public NavigationMenuView(Context context) {
- this(context, null);
- }
-
- public NavigationMenuView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public NavigationMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));
- }
-
- @Override
- public void initialize(MenuBuilder menu) {
-
- }
-
- @Override
- public int getWindowAnimations() {
- return 0;
- }
-
-}
diff --git a/android/support/design/internal/NavigationSubMenu.java b/android/support/design/internal/NavigationSubMenu.java
deleted file mode 100644
index 1ff1e4f..0000000
--- a/android/support/design/internal/NavigationSubMenu.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.support.annotation.RestrictTo;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.support.v7.view.menu.SubMenuBuilder;
-
-/**
- * This is a {@link SubMenuBuilder} that it notifies the parent {@link NavigationMenu} of its menu
- * updates.
- *
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class NavigationSubMenu extends SubMenuBuilder {
-
- public NavigationSubMenu(Context context, NavigationMenu menu, MenuItemImpl item) {
- super(context, menu, item);
- }
-
- @Override
- public void onItemsChanged(boolean structureChanged) {
- super.onItemsChanged(structureChanged);
- ((MenuBuilder) getParentMenu()).onItemsChanged(structureChanged);
- }
-
-}
diff --git a/android/support/design/internal/ParcelableSparseArray.java b/android/support/design/internal/ParcelableSparseArray.java
deleted file mode 100644
index b29000e..0000000
--- a/android/support/design/internal/ParcelableSparseArray.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.RestrictTo;
-import android.util.SparseArray;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class ParcelableSparseArray extends SparseArray<Parcelable> implements Parcelable {
-
- public ParcelableSparseArray() {
- super();
- }
-
- public ParcelableSparseArray(Parcel source, ClassLoader loader) {
- super();
- int size = source.readInt();
- int[] keys = new int[size];
- source.readIntArray(keys);
- Parcelable[] values = source.readParcelableArray(loader);
- for (int i = 0; i < size; ++i) {
- put(keys[i], values[i]);
- }
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel parcel, int flags) {
- int size = size();
- int[] keys = new int[size];
- Parcelable[] values = new Parcelable[size];
- for (int i = 0; i < size; ++i) {
- keys[i] = keyAt(i);
- values[i] = valueAt(i);
- }
- parcel.writeInt(size);
- parcel.writeIntArray(keys);
- parcel.writeParcelableArray(values, flags);
- }
-
- public static final Creator<ParcelableSparseArray> CREATOR =
- new ClassLoaderCreator<ParcelableSparseArray>() {
- @Override
- public ParcelableSparseArray createFromParcel(Parcel source, ClassLoader loader) {
- return new ParcelableSparseArray(source, loader);
- }
-
- @Override
- public ParcelableSparseArray createFromParcel(Parcel source) {
- return new ParcelableSparseArray(source, null);
- }
-
- @Override
- public ParcelableSparseArray[] newArray(int size) {
- return new ParcelableSparseArray[size];
- }
- };
-}
diff --git a/android/support/design/internal/ScrimInsetsFrameLayout.java b/android/support/design/internal/ScrimInsetsFrameLayout.java
deleted file mode 100644
index 38f5b29..0000000
--- a/android/support/design/internal/ScrimInsetsFrameLayout.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.design.R;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.WindowInsetsCompat;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.FrameLayout;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class ScrimInsetsFrameLayout extends FrameLayout {
-
- Drawable mInsetForeground;
-
- Rect mInsets;
-
- private Rect mTempRect = new Rect();
-
- public ScrimInsetsFrameLayout(Context context) {
- this(context, null);
- }
-
- public ScrimInsetsFrameLayout(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public ScrimInsetsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- final TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.ScrimInsetsFrameLayout, defStyleAttr,
- R.style.Widget_Design_ScrimInsetsFrameLayout);
- mInsetForeground = a.getDrawable(R.styleable.ScrimInsetsFrameLayout_insetForeground);
- a.recycle();
- setWillNotDraw(true); // No need to draw until the insets are adjusted
-
- ViewCompat.setOnApplyWindowInsetsListener(this,
- new android.support.v4.view.OnApplyWindowInsetsListener() {
- @Override
- public WindowInsetsCompat onApplyWindowInsets(View v,
- WindowInsetsCompat insets) {
- if (null == mInsets) {
- mInsets = new Rect();
- }
- mInsets.set(insets.getSystemWindowInsetLeft(),
- insets.getSystemWindowInsetTop(),
- insets.getSystemWindowInsetRight(),
- insets.getSystemWindowInsetBottom());
- onInsetsChanged(insets);
- setWillNotDraw(!insets.hasSystemWindowInsets() || mInsetForeground == null);
- ViewCompat.postInvalidateOnAnimation(ScrimInsetsFrameLayout.this);
- return insets.consumeSystemWindowInsets();
- }
- });
- }
-
- @Override
- public void draw(@NonNull Canvas canvas) {
- super.draw(canvas);
-
- int width = getWidth();
- int height = getHeight();
- if (mInsets != null && mInsetForeground != null) {
- int sc = canvas.save();
- canvas.translate(getScrollX(), getScrollY());
-
- // Top
- mTempRect.set(0, 0, width, mInsets.top);
- mInsetForeground.setBounds(mTempRect);
- mInsetForeground.draw(canvas);
-
- // Bottom
- mTempRect.set(0, height - mInsets.bottom, width, height);
- mInsetForeground.setBounds(mTempRect);
- mInsetForeground.draw(canvas);
-
- // Left
- mTempRect.set(0, mInsets.top, mInsets.left, height - mInsets.bottom);
- mInsetForeground.setBounds(mTempRect);
- mInsetForeground.draw(canvas);
-
- // Right
- mTempRect.set(width - mInsets.right, mInsets.top, width, height - mInsets.bottom);
- mInsetForeground.setBounds(mTempRect);
- mInsetForeground.draw(canvas);
-
- canvas.restoreToCount(sc);
- }
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- if (mInsetForeground != null) {
- mInsetForeground.setCallback(this);
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- if (mInsetForeground != null) {
- mInsetForeground.setCallback(null);
- }
- }
-
- protected void onInsetsChanged(WindowInsetsCompat insets) {
- }
-
-}
diff --git a/android/support/design/internal/SnackbarContentLayout.java b/android/support/design/internal/SnackbarContentLayout.java
deleted file mode 100644
index 2abf012..0000000
--- a/android/support/design/internal/SnackbarContentLayout.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * 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.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.support.annotation.RestrictTo;
-import android.support.design.R;
-import android.support.design.widget.BaseTransientBottomBar;
-import android.support.v4.view.ViewCompat;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class SnackbarContentLayout extends LinearLayout implements
- BaseTransientBottomBar.ContentViewCallback {
- private TextView mMessageView;
- private Button mActionView;
-
- private int mMaxWidth;
- private int mMaxInlineActionWidth;
-
- public SnackbarContentLayout(Context context) {
- this(context, null);
- }
-
- public SnackbarContentLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout);
- mMaxWidth = a.getDimensionPixelSize(R.styleable.SnackbarLayout_android_maxWidth, -1);
- mMaxInlineActionWidth = a.getDimensionPixelSize(
- R.styleable.SnackbarLayout_maxActionInlineWidth, -1);
- a.recycle();
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mMessageView = findViewById(R.id.snackbar_text);
- mActionView = findViewById(R.id.snackbar_action);
- }
-
- public TextView getMessageView() {
- return mMessageView;
- }
-
- public Button getActionView() {
- return mActionView;
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- if (mMaxWidth > 0 && getMeasuredWidth() > mMaxWidth) {
- widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY);
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- final int multiLineVPadding = getResources().getDimensionPixelSize(
- R.dimen.design_snackbar_padding_vertical_2lines);
- final int singleLineVPadding = getResources().getDimensionPixelSize(
- R.dimen.design_snackbar_padding_vertical);
- final boolean isMultiLine = mMessageView.getLayout().getLineCount() > 1;
-
- boolean remeasure = false;
- if (isMultiLine && mMaxInlineActionWidth > 0
- && mActionView.getMeasuredWidth() > mMaxInlineActionWidth) {
- if (updateViewsWithinLayout(VERTICAL, multiLineVPadding,
- multiLineVPadding - singleLineVPadding)) {
- remeasure = true;
- }
- } else {
- final int messagePadding = isMultiLine ? multiLineVPadding : singleLineVPadding;
- if (updateViewsWithinLayout(HORIZONTAL, messagePadding, messagePadding)) {
- remeasure = true;
- }
- }
-
- if (remeasure) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- }
-
- private boolean updateViewsWithinLayout(final int orientation,
- final int messagePadTop, final int messagePadBottom) {
- boolean changed = false;
- if (orientation != getOrientation()) {
- setOrientation(orientation);
- changed = true;
- }
- if (mMessageView.getPaddingTop() != messagePadTop
- || mMessageView.getPaddingBottom() != messagePadBottom) {
- updateTopBottomPadding(mMessageView, messagePadTop, messagePadBottom);
- changed = true;
- }
- return changed;
- }
-
- private static void updateTopBottomPadding(View view, int topPadding, int bottomPadding) {
- if (ViewCompat.isPaddingRelative(view)) {
- ViewCompat.setPaddingRelative(view,
- ViewCompat.getPaddingStart(view), topPadding,
- ViewCompat.getPaddingEnd(view), bottomPadding);
- } else {
- view.setPadding(view.getPaddingLeft(), topPadding,
- view.getPaddingRight(), bottomPadding);
- }
- }
-
- @Override
- public void animateContentIn(int delay, int duration) {
- mMessageView.setAlpha(0f);
- mMessageView.animate().alpha(1f).setDuration(duration)
- .setStartDelay(delay).start();
-
- if (mActionView.getVisibility() == VISIBLE) {
- mActionView.setAlpha(0f);
- mActionView.animate().alpha(1f).setDuration(duration)
- .setStartDelay(delay).start();
- }
- }
-
- @Override
- public void animateContentOut(int delay, int duration) {
- mMessageView.setAlpha(1f);
- mMessageView.animate().alpha(0f).setDuration(duration)
- .setStartDelay(delay).start();
-
- if (mActionView.getVisibility() == VISIBLE) {
- mActionView.setAlpha(1f);
- mActionView.animate().alpha(0f).setDuration(duration)
- .setStartDelay(delay).start();
- }
- }
-}
diff --git a/android/support/design/internal/TextScale.java b/android/support/design/internal/TextScale.java
deleted file mode 100644
index 06c9472..0000000
--- a/android/support/design/internal/TextScale.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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.support.design.internal;
-
-import android.animation.Animator;
-import android.animation.ValueAnimator;
-import android.support.annotation.RequiresApi;
-import android.support.annotation.RestrictTo;
-import android.support.transition.Transition;
-import android.support.transition.TransitionValues;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import java.util.Map;
-
-/**
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@RequiresApi(14)
-public class TextScale extends Transition {
- private static final String PROPNAME_SCALE = "android:textscale:scale";
-
- @Override
- public void captureStartValues(TransitionValues transitionValues) {
- captureValues(transitionValues);
- }
-
- @Override
- public void captureEndValues(TransitionValues transitionValues) {
- captureValues(transitionValues);
- }
-
- private void captureValues(TransitionValues transitionValues) {
- if (transitionValues.view instanceof TextView) {
- TextView textview = (TextView) transitionValues.view;
- transitionValues.values.put(PROPNAME_SCALE, textview.getScaleX());
- }
- }
-
- @Override
- public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
- TransitionValues endValues) {
- if (startValues == null || endValues == null || !(startValues.view instanceof TextView)
- || !(endValues.view instanceof TextView)) {
- return null;
- }
- final TextView view = (TextView) endValues.view;
- Map<String, Object> startVals = startValues.values;
- Map<String, Object> endVals = endValues.values;
- final float startSize = startVals.get(PROPNAME_SCALE) != null ? (float) startVals.get(
- PROPNAME_SCALE) : 1f;
- final float endSize = endVals.get(PROPNAME_SCALE) != null ? (float) endVals.get(
- PROPNAME_SCALE) : 1f;
- if (startSize == endSize) {
- return null;
- }
-
- ValueAnimator animator = ValueAnimator.ofFloat(startSize, endSize);
-
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator valueAnimator) {
- float animatedValue = (float) valueAnimator.getAnimatedValue();
- view.setScaleX(animatedValue);
- view.setScaleY(animatedValue);
- }
- });
- return animator;
- }
-}
diff --git a/android/support/design/internal/package-info.java b/android/support/design/internal/package-info.java
deleted file mode 100644
index 6b6f7bb..0000000
--- a/android/support/design/internal/package-info.java
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-package android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.support.annotation.RestrictTo;
diff --git a/android/support/design/widget/AnimationUtils.java b/android/support/design/widget/AnimationUtils.java
deleted file mode 100644
index 3613afd..0000000
--- a/android/support/design/widget/AnimationUtils.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.support.v4.view.animation.FastOutLinearInInterpolator;
-import android.support.v4.view.animation.FastOutSlowInInterpolator;
-import android.support.v4.view.animation.LinearOutSlowInInterpolator;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Interpolator;
-import android.view.animation.LinearInterpolator;
-
-class AnimationUtils {
-
- static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
- static final Interpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator();
- static final Interpolator FAST_OUT_LINEAR_IN_INTERPOLATOR = new FastOutLinearInInterpolator();
- static final Interpolator LINEAR_OUT_SLOW_IN_INTERPOLATOR = new LinearOutSlowInInterpolator();
- static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
-
- /**
- * Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}.
- */
- static float lerp(float startValue, float endValue, float fraction) {
- return startValue + (fraction * (endValue - startValue));
- }
-
- static int lerp(int startValue, int endValue, float fraction) {
- return startValue + Math.round(fraction * (endValue - startValue));
- }
-
-}
diff --git a/android/support/design/widget/AppBarLayout.java b/android/support/design/widget/AppBarLayout.java
deleted file mode 100644
index 8304cd6..0000000
--- a/android/support/design/widget/AppBarLayout.java
+++ /dev/null
@@ -1,1474 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Rect;
-import android.os.Build;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RequiresApi;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.VisibleForTesting;
-import android.support.design.R;
-import android.support.v4.math.MathUtils;
-import android.support.v4.util.ObjectsCompat;
-import android.support.v4.view.AbsSavedState;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.WindowInsetsCompat;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.animation.Interpolator;
-import android.widget.LinearLayout;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * AppBarLayout is a vertical {@link LinearLayout} which implements many of the features of
- * material designs app bar concept, namely scrolling gestures.
- * <p>
- * Children should provide their desired scrolling behavior through
- * {@link LayoutParams#setScrollFlags(int)} and the associated layout xml attribute:
- * {@code app:layout_scrollFlags}.
- *
- * <p>
- * This view depends heavily on being used as a direct child within a {@link CoordinatorLayout}.
- * If you use AppBarLayout within a different {@link ViewGroup}, most of it's functionality will
- * not work.
- * <p>
- * AppBarLayout also requires a separate scrolling sibling in order to know when to scroll.
- * The binding is done through the {@link ScrollingViewBehavior} behavior class, meaning that you
- * should set your scrolling view's behavior to be an instance of {@link ScrollingViewBehavior}.
- * A string resource containing the full class name is available.
- *
- * <pre>
- * <android.support.design.widget.CoordinatorLayout
- * xmlns:android="http://schemas.android.com/apk/res/android"
- * xmlns:app="http://schemas.android.com/apk/res-auto"
- * android:layout_width="match_parent"
- * android:layout_height="match_parent">
- *
- * <android.support.v4.widget.NestedScrollView
- * android:layout_width="match_parent"
- * android:layout_height="match_parent"
- * app:layout_behavior="@string/appbar_scrolling_view_behavior">
- *
- * <!-- Your scrolling content -->
- *
- * </android.support.v4.widget.NestedScrollView>
- *
- * <android.support.design.widget.AppBarLayout
- * android:layout_height="wrap_content"
- * android:layout_width="match_parent">
- *
- * <android.support.v7.widget.Toolbar
- * ...
- * app:layout_scrollFlags="scroll|enterAlways"/>
- *
- * <android.support.design.widget.TabLayout
- * ...
- * app:layout_scrollFlags="scroll|enterAlways"/>
- *
- * </android.support.design.widget.AppBarLayout>
- *
- * </android.support.design.widget.CoordinatorLayout>
- * </pre>
- *
- * @see <a href="http://www.google.com/design/spec/layout/structure.html#structure-app-bar">
- * http://www.google.com/design/spec/layout/structure.html#structure-app-bar</a>
- */
[email protected](AppBarLayout.Behavior.class)
-public class AppBarLayout extends LinearLayout {
-
- static final int PENDING_ACTION_NONE = 0x0;
- static final int PENDING_ACTION_EXPANDED = 0x1;
- static final int PENDING_ACTION_COLLAPSED = 0x2;
- static final int PENDING_ACTION_ANIMATE_ENABLED = 0x4;
- static final int PENDING_ACTION_FORCE = 0x8;
-
- /**
- * Interface definition for a callback to be invoked when an {@link AppBarLayout}'s vertical
- * offset changes.
- */
- public interface OnOffsetChangedListener {
- /**
- * Called when the {@link AppBarLayout}'s layout offset has been changed. This allows
- * child views to implement custom behavior based on the offset (for instance pinning a
- * view at a certain y value).
- *
- * @param appBarLayout the {@link AppBarLayout} which offset has changed
- * @param verticalOffset the vertical offset for the parent {@link AppBarLayout}, in px
- */
- void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset);
- }
-
- private static final int INVALID_SCROLL_RANGE = -1;
-
- private int mTotalScrollRange = INVALID_SCROLL_RANGE;
- private int mDownPreScrollRange = INVALID_SCROLL_RANGE;
- private int mDownScrollRange = INVALID_SCROLL_RANGE;
-
- private boolean mHaveChildWithInterpolator;
-
- private int mPendingAction = PENDING_ACTION_NONE;
-
- private WindowInsetsCompat mLastInsets;
-
- private List<OnOffsetChangedListener> mListeners;
-
- private boolean mCollapsible;
- private boolean mCollapsed;
-
- private int[] mTmpStatesArray;
-
- public AppBarLayout(Context context) {
- this(context, null);
- }
-
- public AppBarLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- setOrientation(VERTICAL);
-
- ThemeUtils.checkAppCompatTheme(context);
-
- if (Build.VERSION.SDK_INT >= 21) {
- // Use the bounds view outline provider so that we cast a shadow, even without a
- // background
- ViewUtilsLollipop.setBoundsViewOutlineProvider(this);
-
- // If we're running on API 21+, we should reset any state list animator from our
- // default style
- ViewUtilsLollipop.setStateListAnimatorFromAttrs(this, attrs, 0,
- R.style.Widget_Design_AppBarLayout);
- }
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AppBarLayout,
- 0, R.style.Widget_Design_AppBarLayout);
- ViewCompat.setBackground(this, a.getDrawable(R.styleable.AppBarLayout_android_background));
- if (a.hasValue(R.styleable.AppBarLayout_expanded)) {
- setExpanded(a.getBoolean(R.styleable.AppBarLayout_expanded, false), false, false);
- }
- if (Build.VERSION.SDK_INT >= 21 && a.hasValue(R.styleable.AppBarLayout_elevation)) {
- ViewUtilsLollipop.setDefaultAppBarLayoutStateListAnimator(
- this, a.getDimensionPixelSize(R.styleable.AppBarLayout_elevation, 0));
- }
- if (Build.VERSION.SDK_INT >= 26) {
- // In O+, we have these values set in the style. Since there is no defStyleAttr for
- // AppBarLayout at the AppCompat level, check for these attributes here.
- if (a.hasValue(R.styleable.AppBarLayout_android_keyboardNavigationCluster)) {
- this.setKeyboardNavigationCluster(a.getBoolean(
- R.styleable.AppBarLayout_android_keyboardNavigationCluster, false));
- }
- if (a.hasValue(R.styleable.AppBarLayout_android_touchscreenBlocksFocus)) {
- this.setTouchscreenBlocksFocus(a.getBoolean(
- R.styleable.AppBarLayout_android_touchscreenBlocksFocus, false));
- }
- }
- a.recycle();
-
- ViewCompat.setOnApplyWindowInsetsListener(this,
- new android.support.v4.view.OnApplyWindowInsetsListener() {
- @Override
- public WindowInsetsCompat onApplyWindowInsets(View v,
- WindowInsetsCompat insets) {
- return onWindowInsetChanged(insets);
- }
- });
- }
-
- /**
- * Add a listener that will be called when the offset of this {@link AppBarLayout} changes.
- *
- * @param listener The listener that will be called when the offset changes.]
- *
- * @see #removeOnOffsetChangedListener(OnOffsetChangedListener)
- */
- public void addOnOffsetChangedListener(OnOffsetChangedListener listener) {
- if (mListeners == null) {
- mListeners = new ArrayList<>();
- }
- if (listener != null && !mListeners.contains(listener)) {
- mListeners.add(listener);
- }
- }
-
- /**
- * Remove the previously added {@link OnOffsetChangedListener}.
- *
- * @param listener the listener to remove.
- */
- public void removeOnOffsetChangedListener(OnOffsetChangedListener listener) {
- if (mListeners != null && listener != null) {
- mListeners.remove(listener);
- }
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- invalidateScrollRanges();
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
- invalidateScrollRanges();
-
- mHaveChildWithInterpolator = false;
- for (int i = 0, z = getChildCount(); i < z; i++) {
- final View child = getChildAt(i);
- final LayoutParams childLp = (LayoutParams) child.getLayoutParams();
- final Interpolator interpolator = childLp.getScrollInterpolator();
-
- if (interpolator != null) {
- mHaveChildWithInterpolator = true;
- break;
- }
- }
-
- updateCollapsible();
- }
-
- private void updateCollapsible() {
- boolean haveCollapsibleChild = false;
- for (int i = 0, z = getChildCount(); i < z; i++) {
- if (((LayoutParams) getChildAt(i).getLayoutParams()).isCollapsible()) {
- haveCollapsibleChild = true;
- break;
- }
- }
- setCollapsibleState(haveCollapsibleChild);
- }
-
- private void invalidateScrollRanges() {
- // Invalidate the scroll ranges
- mTotalScrollRange = INVALID_SCROLL_RANGE;
- mDownPreScrollRange = INVALID_SCROLL_RANGE;
- mDownScrollRange = INVALID_SCROLL_RANGE;
- }
-
- @Override
- public void setOrientation(int orientation) {
- if (orientation != VERTICAL) {
- throw new IllegalArgumentException("AppBarLayout is always vertical and does"
- + " not support horizontal orientation");
- }
- super.setOrientation(orientation);
- }
-
- /**
- * Sets whether this {@link AppBarLayout} is expanded or not, animating if it has already
- * been laid out.
- *
- * <p>As with {@link AppBarLayout}'s scrolling, this method relies on this layout being a
- * direct child of a {@link CoordinatorLayout}.</p>
- *
- * @param expanded true if the layout should be fully expanded, false if it should
- * be fully collapsed
- *
- * @attr ref android.support.design.R.styleable#AppBarLayout_expanded
- */
- public void setExpanded(boolean expanded) {
- setExpanded(expanded, ViewCompat.isLaidOut(this));
- }
-
- /**
- * Sets whether this {@link AppBarLayout} is expanded or not.
- *
- * <p>As with {@link AppBarLayout}'s scrolling, this method relies on this layout being a
- * direct child of a {@link CoordinatorLayout}.</p>
- *
- * @param expanded true if the layout should be fully expanded, false if it should
- * be fully collapsed
- * @param animate Whether to animate to the new state
- *
- * @attr ref android.support.design.R.styleable#AppBarLayout_expanded
- */
- public void setExpanded(boolean expanded, boolean animate) {
- setExpanded(expanded, animate, true);
- }
-
- private void setExpanded(boolean expanded, boolean animate, boolean force) {
- mPendingAction = (expanded ? PENDING_ACTION_EXPANDED : PENDING_ACTION_COLLAPSED)
- | (animate ? PENDING_ACTION_ANIMATE_ENABLED : 0)
- | (force ? PENDING_ACTION_FORCE : 0);
- requestLayout();
- }
-
- @Override
- protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
- return p instanceof LayoutParams;
- }
-
- @Override
- protected LayoutParams generateDefaultLayoutParams() {
- return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
- }
-
- @Override
- public LayoutParams generateLayoutParams(AttributeSet attrs) {
- return new LayoutParams(getContext(), attrs);
- }
-
- @Override
- protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
- if (Build.VERSION.SDK_INT >= 19 && p instanceof LinearLayout.LayoutParams) {
- return new LayoutParams((LinearLayout.LayoutParams) p);
- } else if (p instanceof MarginLayoutParams) {
- return new LayoutParams((MarginLayoutParams) p);
- }
- return new LayoutParams(p);
- }
-
- boolean hasChildWithInterpolator() {
- return mHaveChildWithInterpolator;
- }
-
- /**
- * Returns the scroll range of all children.
- *
- * @return the scroll range in px
- */
- public final int getTotalScrollRange() {
- if (mTotalScrollRange != INVALID_SCROLL_RANGE) {
- return mTotalScrollRange;
- }
-
- int range = 0;
- for (int i = 0, z = getChildCount(); i < z; i++) {
- final View child = getChildAt(i);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final int childHeight = child.getMeasuredHeight();
- final int flags = lp.mScrollFlags;
-
- if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
- // We're set to scroll so add the child's height
- range += childHeight + lp.topMargin + lp.bottomMargin;
-
- if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
- // For a collapsing scroll, we to take the collapsed height into account.
- // We also break straight away since later views can't scroll beneath
- // us
- range -= ViewCompat.getMinimumHeight(child);
- break;
- }
- } else {
- // As soon as a view doesn't have the scroll flag, we end the range calculation.
- // This is because views below can not scroll under a fixed view.
- break;
- }
- }
- return mTotalScrollRange = Math.max(0, range - getTopInset());
- }
-
- boolean hasScrollableChildren() {
- return getTotalScrollRange() != 0;
- }
-
- /**
- * Return the scroll range when scrolling up from a nested pre-scroll.
- */
- int getUpNestedPreScrollRange() {
- return getTotalScrollRange();
- }
-
- /**
- * Return the scroll range when scrolling down from a nested pre-scroll.
- */
- int getDownNestedPreScrollRange() {
- if (mDownPreScrollRange != INVALID_SCROLL_RANGE) {
- // If we already have a valid value, return it
- return mDownPreScrollRange;
- }
-
- int range = 0;
- for (int i = getChildCount() - 1; i >= 0; i--) {
- final View child = getChildAt(i);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final int childHeight = child.getMeasuredHeight();
- final int flags = lp.mScrollFlags;
-
- if ((flags & LayoutParams.FLAG_QUICK_RETURN) == LayoutParams.FLAG_QUICK_RETURN) {
- // First take the margin into account
- range += lp.topMargin + lp.bottomMargin;
- // The view has the quick return flag combination...
- if ((flags & LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED) != 0) {
- // If they're set to enter collapsed, use the minimum height
- range += ViewCompat.getMinimumHeight(child);
- } else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
- // Only enter by the amount of the collapsed height
- range += childHeight - ViewCompat.getMinimumHeight(child);
- } else {
- // Else use the full height (minus the top inset)
- range += childHeight - getTopInset();
- }
- } else if (range > 0) {
- // If we've hit an non-quick return scrollable view, and we've already hit a
- // quick return view, return now
- break;
- }
- }
- return mDownPreScrollRange = Math.max(0, range);
- }
-
- /**
- * Return the scroll range when scrolling down from a nested scroll.
- */
- int getDownNestedScrollRange() {
- if (mDownScrollRange != INVALID_SCROLL_RANGE) {
- // If we already have a valid value, return it
- return mDownScrollRange;
- }
-
- int range = 0;
- for (int i = 0, z = getChildCount(); i < z; i++) {
- final View child = getChildAt(i);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- int childHeight = child.getMeasuredHeight();
- childHeight += lp.topMargin + lp.bottomMargin;
-
- final int flags = lp.mScrollFlags;
-
- if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
- // We're set to scroll so add the child's height
- range += childHeight;
-
- if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
- // For a collapsing exit scroll, we to take the collapsed height into account.
- // We also break the range straight away since later views can't scroll
- // beneath us
- range -= ViewCompat.getMinimumHeight(child) + getTopInset();
- break;
- }
- } else {
- // As soon as a view doesn't have the scroll flag, we end the range calculation.
- // This is because views below can not scroll under a fixed view.
- break;
- }
- }
- return mDownScrollRange = Math.max(0, range);
- }
-
- void dispatchOffsetUpdates(int offset) {
- // Iterate backwards through the list so that most recently added listeners
- // get the first chance to decide
- if (mListeners != null) {
- for (int i = 0, z = mListeners.size(); i < z; i++) {
- final OnOffsetChangedListener listener = mListeners.get(i);
- if (listener != null) {
- listener.onOffsetChanged(this, offset);
- }
- }
- }
- }
-
- final int getMinimumHeightForVisibleOverlappingContent() {
- final int topInset = getTopInset();
- final int minHeight = ViewCompat.getMinimumHeight(this);
- if (minHeight != 0) {
- // If this layout has a min height, use it (doubled)
- return (minHeight * 2) + topInset;
- }
-
- // Otherwise, we'll use twice the min height of our last child
- final int childCount = getChildCount();
- final int lastChildMinHeight = childCount >= 1
- ? ViewCompat.getMinimumHeight(getChildAt(childCount - 1)) : 0;
- if (lastChildMinHeight != 0) {
- return (lastChildMinHeight * 2) + topInset;
- }
-
- // If we reach here then we don't have a min height explicitly set. Instead we'll take a
- // guess at 1/3 of our height being visible
- return getHeight() / 3;
- }
-
- @Override
- protected int[] onCreateDrawableState(int extraSpace) {
- if (mTmpStatesArray == null) {
- // Note that we can't allocate this at the class level (in declaration) since
- // some paths in super View constructor are going to call this method before
- // that
- mTmpStatesArray = new int[2];
- }
- final int[] extraStates = mTmpStatesArray;
- final int[] states = super.onCreateDrawableState(extraSpace + extraStates.length);
-
- extraStates[0] = mCollapsible ? R.attr.state_collapsible : -R.attr.state_collapsible;
- extraStates[1] = mCollapsible && mCollapsed
- ? R.attr.state_collapsed : -R.attr.state_collapsed;
-
- return mergeDrawableStates(states, extraStates);
- }
-
- /**
- * Sets whether the AppBarLayout has collapsible children or not.
- *
- * @return true if the collapsible state changed
- */
- private boolean setCollapsibleState(boolean collapsible) {
- if (mCollapsible != collapsible) {
- mCollapsible = collapsible;
- refreshDrawableState();
- return true;
- }
- return false;
- }
-
- /**
- * Sets whether the AppBarLayout is in a collapsed state or not.
- *
- * @return true if the collapsed state changed
- */
- boolean setCollapsedState(boolean collapsed) {
- if (mCollapsed != collapsed) {
- mCollapsed = collapsed;
- refreshDrawableState();
- return true;
- }
- return false;
- }
-
- /**
- * @deprecated target elevation is now deprecated. AppBarLayout's elevation is now
- * controlled via a {@link android.animation.StateListAnimator}. If a target
- * elevation is set, either by this method or the {@code app:elevation} attribute,
- * a new state list animator is created which uses the given {@code elevation} value.
- *
- * @attr ref android.support.design.R.styleable#AppBarLayout_elevation
- */
- @Deprecated
- public void setTargetElevation(float elevation) {
- if (Build.VERSION.SDK_INT >= 21) {
- ViewUtilsLollipop.setDefaultAppBarLayoutStateListAnimator(this, elevation);
- }
- }
-
- /**
- * @deprecated target elevation is now deprecated. AppBarLayout's elevation is now
- * controlled via a {@link android.animation.StateListAnimator}. This method now
- * always returns 0.
- */
- @Deprecated
- public float getTargetElevation() {
- return 0;
- }
-
- int getPendingAction() {
- return mPendingAction;
- }
-
- void resetPendingAction() {
- mPendingAction = PENDING_ACTION_NONE;
- }
-
- @VisibleForTesting
- final int getTopInset() {
- return mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
- }
-
- WindowInsetsCompat onWindowInsetChanged(final WindowInsetsCompat insets) {
- WindowInsetsCompat newInsets = null;
-
- if (ViewCompat.getFitsSystemWindows(this)) {
- // If we're set to fit system windows, keep the insets
- newInsets = insets;
- }
-
- // If our insets have changed, keep them and invalidate the scroll ranges...
- if (!ObjectsCompat.equals(mLastInsets, newInsets)) {
- mLastInsets = newInsets;
- invalidateScrollRanges();
- }
-
- return insets;
- }
-
- public static class LayoutParams extends LinearLayout.LayoutParams {
-
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef(flag=true, value={
- SCROLL_FLAG_SCROLL,
- SCROLL_FLAG_EXIT_UNTIL_COLLAPSED,
- SCROLL_FLAG_ENTER_ALWAYS,
- SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED,
- SCROLL_FLAG_SNAP
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface ScrollFlags {}
-
- /**
- * The view will be scroll in direct relation to scroll events. This flag needs to be
- * set for any of the other flags to take effect. If any sibling views
- * before this one do not have this flag, then this value has no effect.
- */
- public static final int SCROLL_FLAG_SCROLL = 0x1;
-
- /**
- * When exiting (scrolling off screen) the view will be scrolled until it is
- * 'collapsed'. The collapsed height is defined by the view's minimum height.
- *
- * @see ViewCompat#getMinimumHeight(View)
- * @see View#setMinimumHeight(int)
- */
- public static final int SCROLL_FLAG_EXIT_UNTIL_COLLAPSED = 0x2;
-
- /**
- * When entering (scrolling on screen) the view will scroll on any downwards
- * scroll event, regardless of whether the scrolling view is also scrolling. This
- * is commonly referred to as the 'quick return' pattern.
- */
- public static final int SCROLL_FLAG_ENTER_ALWAYS = 0x4;
-
- /**
- * An additional flag for 'enterAlways' which modifies the returning view to
- * only initially scroll back to it's collapsed height. Once the scrolling view has
- * reached the end of it's scroll range, the remainder of this view will be scrolled
- * into view. The collapsed height is defined by the view's minimum height.
- *
- * @see ViewCompat#getMinimumHeight(View)
- * @see View#setMinimumHeight(int)
- */
- public static final int SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED = 0x8;
-
- /**
- * Upon a scroll ending, if the view is only partially visible then it will be snapped
- * and scrolled to it's closest edge. For example, if the view only has it's bottom 25%
- * displayed, it will be scrolled off screen completely. Conversely, if it's bottom 75%
- * is visible then it will be scrolled fully into view.
- */
- public static final int SCROLL_FLAG_SNAP = 0x10;
-
- /**
- * Internal flags which allows quick checking features
- */
- static final int FLAG_QUICK_RETURN = SCROLL_FLAG_SCROLL | SCROLL_FLAG_ENTER_ALWAYS;
- static final int FLAG_SNAP = SCROLL_FLAG_SCROLL | SCROLL_FLAG_SNAP;
- static final int COLLAPSIBLE_FLAGS = SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
- | SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED;
-
- int mScrollFlags = SCROLL_FLAG_SCROLL;
- Interpolator mScrollInterpolator;
-
- public LayoutParams(Context c, AttributeSet attrs) {
- super(c, attrs);
- TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.AppBarLayout_Layout);
- mScrollFlags = a.getInt(R.styleable.AppBarLayout_Layout_layout_scrollFlags, 0);
- if (a.hasValue(R.styleable.AppBarLayout_Layout_layout_scrollInterpolator)) {
- int resId = a.getResourceId(
- R.styleable.AppBarLayout_Layout_layout_scrollInterpolator, 0);
- mScrollInterpolator = android.view.animation.AnimationUtils.loadInterpolator(
- c, resId);
- }
- a.recycle();
- }
-
- public LayoutParams(int width, int height) {
- super(width, height);
- }
-
- public LayoutParams(int width, int height, float weight) {
- super(width, height, weight);
- }
-
- public LayoutParams(ViewGroup.LayoutParams p) {
- super(p);
- }
-
- public LayoutParams(MarginLayoutParams source) {
- super(source);
- }
-
- @RequiresApi(19)
- public LayoutParams(LinearLayout.LayoutParams source) {
- // The copy constructor called here only exists on API 19+.
- super(source);
- }
-
- @RequiresApi(19)
- public LayoutParams(LayoutParams source) {
- // The copy constructor called here only exists on API 19+.
- super(source);
- mScrollFlags = source.mScrollFlags;
- mScrollInterpolator = source.mScrollInterpolator;
- }
-
- /**
- * Set the scrolling flags.
- *
- * @param flags bitwise int of {@link #SCROLL_FLAG_SCROLL},
- * {@link #SCROLL_FLAG_EXIT_UNTIL_COLLAPSED}, {@link #SCROLL_FLAG_ENTER_ALWAYS},
- * {@link #SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED} and {@link #SCROLL_FLAG_SNAP }.
- *
- * @see #getScrollFlags()
- *
- * @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollFlags
- */
- public void setScrollFlags(@ScrollFlags int flags) {
- mScrollFlags = flags;
- }
-
- /**
- * Returns the scrolling flags.
- *
- * @see #setScrollFlags(int)
- *
- * @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollFlags
- */
- @ScrollFlags
- public int getScrollFlags() {
- return mScrollFlags;
- }
-
- /**
- * Set the interpolator to when scrolling the view associated with this
- * {@link LayoutParams}.
- *
- * @param interpolator the interpolator to use, or null to use normal 1-to-1 scrolling.
- *
- * @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollInterpolator
- * @see #getScrollInterpolator()
- */
- public void setScrollInterpolator(Interpolator interpolator) {
- mScrollInterpolator = interpolator;
- }
-
- /**
- * Returns the {@link Interpolator} being used for scrolling the view associated with this
- * {@link LayoutParams}. Null indicates 'normal' 1-to-1 scrolling.
- *
- * @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollInterpolator
- * @see #setScrollInterpolator(Interpolator)
- */
- public Interpolator getScrollInterpolator() {
- return mScrollInterpolator;
- }
-
- /**
- * Returns true if the scroll flags are compatible for 'collapsing'
- */
- boolean isCollapsible() {
- return (mScrollFlags & SCROLL_FLAG_SCROLL) == SCROLL_FLAG_SCROLL
- && (mScrollFlags & COLLAPSIBLE_FLAGS) != 0;
- }
- }
-
- /**
- * The default {@link Behavior} for {@link AppBarLayout}. Implements the necessary nested
- * scroll handling with offsetting.
- */
- public static class Behavior extends HeaderBehavior<AppBarLayout> {
- private static final int MAX_OFFSET_ANIMATION_DURATION = 600; // ms
- private static final int INVALID_POSITION = -1;
-
- /**
- * Callback to allow control over any {@link AppBarLayout} dragging.
- */
- public static abstract class DragCallback {
- /**
- * Allows control over whether the given {@link AppBarLayout} can be dragged or not.
- *
- * <p>Dragging is defined as a direct touch on the AppBarLayout with movement. This
- * call does not affect any nested scrolling.</p>
- *
- * @return true if we are in a position to scroll the AppBarLayout via a drag, false
- * if not.
- */
- public abstract boolean canDrag(@NonNull AppBarLayout appBarLayout);
- }
-
- private int mOffsetDelta;
- private ValueAnimator mOffsetAnimator;
-
- private int mOffsetToChildIndexOnLayout = INVALID_POSITION;
- private boolean mOffsetToChildIndexOnLayoutIsMinHeight;
- private float mOffsetToChildIndexOnLayoutPerc;
-
- private WeakReference<View> mLastNestedScrollingChildRef;
- private DragCallback mOnDragCallback;
-
- public Behavior() {}
-
- public Behavior(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
- View directTargetChild, View target, int nestedScrollAxes, int type) {
- // Return true if we're nested scrolling vertically, and we have scrollable children
- // and the scrolling view is big enough to scroll
- final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
- && child.hasScrollableChildren()
- && parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();
-
- if (started && mOffsetAnimator != null) {
- // Cancel any offset animation
- mOffsetAnimator.cancel();
- }
-
- // A new nested scroll has started so clear out the previous ref
- mLastNestedScrollingChildRef = null;
-
- return started;
- }
-
- @Override
- public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
- View target, int dx, int dy, int[] consumed, int type) {
- if (dy != 0) {
- int min, max;
- if (dy < 0) {
- // We're scrolling down
- min = -child.getTotalScrollRange();
- max = min + child.getDownNestedPreScrollRange();
- } else {
- // We're scrolling up
- min = -child.getUpNestedPreScrollRange();
- max = 0;
- }
- if (min != max) {
- consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
- }
- }
- }
-
- @Override
- public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
- View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
- int type) {
- if (dyUnconsumed < 0) {
- // If the scrolling view is scrolling down but not consuming, it's probably be at
- // the top of it's content
- scroll(coordinatorLayout, child, dyUnconsumed,
- -child.getDownNestedScrollRange(), 0);
- }
- }
-
- @Override
- public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl,
- View target, int type) {
- if (type == ViewCompat.TYPE_TOUCH) {
- // If we haven't been flung then let's see if the current view has been set to snap
- snapToChildIfNeeded(coordinatorLayout, abl);
- }
-
- // Keep a reference to the previous nested scrolling child
- mLastNestedScrollingChildRef = new WeakReference<>(target);
- }
-
- /**
- * Set a callback to control any {@link AppBarLayout} dragging.
- *
- * @param callback the callback to use, or {@code null} to use the default behavior.
- */
- public void setDragCallback(@Nullable DragCallback callback) {
- mOnDragCallback = callback;
- }
-
- private void animateOffsetTo(final CoordinatorLayout coordinatorLayout,
- final AppBarLayout child, final int offset, float velocity) {
- final int distance = Math.abs(getTopBottomOffsetForScrollingSibling() - offset);
-
- final int duration;
- velocity = Math.abs(velocity);
- if (velocity > 0) {
- duration = 3 * Math.round(1000 * (distance / velocity));
- } else {
- final float distanceRatio = (float) distance / child.getHeight();
- duration = (int) ((distanceRatio + 1) * 150);
- }
-
- animateOffsetWithDuration(coordinatorLayout, child, offset, duration);
- }
-
- private void animateOffsetWithDuration(final CoordinatorLayout coordinatorLayout,
- final AppBarLayout child, final int offset, final int duration) {
- final int currentOffset = getTopBottomOffsetForScrollingSibling();
- if (currentOffset == offset) {
- if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) {
- mOffsetAnimator.cancel();
- }
- return;
- }
-
- if (mOffsetAnimator == null) {
- mOffsetAnimator = new ValueAnimator();
- mOffsetAnimator.setInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);
- mOffsetAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- setHeaderTopBottomOffset(coordinatorLayout, child,
- (int) animation.getAnimatedValue());
- }
- });
- } else {
- mOffsetAnimator.cancel();
- }
-
- mOffsetAnimator.setDuration(Math.min(duration, MAX_OFFSET_ANIMATION_DURATION));
- mOffsetAnimator.setIntValues(currentOffset, offset);
- mOffsetAnimator.start();
- }
-
- private int getChildIndexOnOffset(AppBarLayout abl, final int offset) {
- for (int i = 0, count = abl.getChildCount(); i < count; i++) {
- View child = abl.getChildAt(i);
- if (child.getTop() <= -offset && child.getBottom() >= -offset) {
- return i;
- }
- }
- return -1;
- }
-
- private void snapToChildIfNeeded(CoordinatorLayout coordinatorLayout, AppBarLayout abl) {
- final int offset = getTopBottomOffsetForScrollingSibling();
- final int offsetChildIndex = getChildIndexOnOffset(abl, offset);
- if (offsetChildIndex >= 0) {
- final View offsetChild = abl.getChildAt(offsetChildIndex);
- final LayoutParams lp = (LayoutParams) offsetChild.getLayoutParams();
- final int flags = lp.getScrollFlags();
-
- if ((flags & LayoutParams.FLAG_SNAP) == LayoutParams.FLAG_SNAP) {
- // We're set the snap, so animate the offset to the nearest edge
- int snapTop = -offsetChild.getTop();
- int snapBottom = -offsetChild.getBottom();
-
- if (offsetChildIndex == abl.getChildCount() - 1) {
- // If this is the last child, we need to take the top inset into account
- snapBottom += abl.getTopInset();
- }
-
- if (checkFlag(flags, LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED)) {
- // If the view is set only exit until it is collapsed, we'll abide by that
- snapBottom += ViewCompat.getMinimumHeight(offsetChild);
- } else if (checkFlag(flags, LayoutParams.FLAG_QUICK_RETURN
- | LayoutParams.SCROLL_FLAG_ENTER_ALWAYS)) {
- // If it's set to always enter collapsed, it actually has two states. We
- // select the state and then snap within the state
- final int seam = snapBottom + ViewCompat.getMinimumHeight(offsetChild);
- if (offset < seam) {
- snapTop = seam;
- } else {
- snapBottom = seam;
- }
- }
-
- final int newOffset = offset < (snapBottom + snapTop) / 2
- ? snapBottom
- : snapTop;
- animateOffsetTo(coordinatorLayout, abl,
- MathUtils.clamp(newOffset, -abl.getTotalScrollRange(), 0), 0);
- }
- }
- }
-
- private static boolean checkFlag(final int flags, final int check) {
- return (flags & check) == check;
- }
-
- @Override
- public boolean onMeasureChild(CoordinatorLayout parent, AppBarLayout child,
- int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
- int heightUsed) {
- final CoordinatorLayout.LayoutParams lp =
- (CoordinatorLayout.LayoutParams) child.getLayoutParams();
- if (lp.height == CoordinatorLayout.LayoutParams.WRAP_CONTENT) {
- // If the view is set to wrap on it's height, CoordinatorLayout by default will
- // cap the view at the CoL's height. Since the AppBarLayout can scroll, this isn't
- // what we actually want, so we measure it ourselves with an unspecified spec to
- // allow the child to be larger than it's parent
- parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed,
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightUsed);
- return true;
- }
-
- // Let the parent handle it as normal
- return super.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed,
- parentHeightMeasureSpec, heightUsed);
- }
-
- @Override
- public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl,
- int layoutDirection) {
- boolean handled = super.onLayoutChild(parent, abl, layoutDirection);
-
- // The priority for for actions here is (first which is true wins):
- // 1. forced pending actions
- // 2. offsets for restorations
- // 3. non-forced pending actions
- final int pendingAction = abl.getPendingAction();
- if (mOffsetToChildIndexOnLayout >= 0 && (pendingAction & PENDING_ACTION_FORCE) == 0) {
- View child = abl.getChildAt(mOffsetToChildIndexOnLayout);
- int offset = -child.getBottom();
- if (mOffsetToChildIndexOnLayoutIsMinHeight) {
- offset += ViewCompat.getMinimumHeight(child) + abl.getTopInset();
- } else {
- offset += Math.round(child.getHeight() * mOffsetToChildIndexOnLayoutPerc);
- }
- setHeaderTopBottomOffset(parent, abl, offset);
- } else if (pendingAction != PENDING_ACTION_NONE) {
- final boolean animate = (pendingAction & PENDING_ACTION_ANIMATE_ENABLED) != 0;
- if ((pendingAction & PENDING_ACTION_COLLAPSED) != 0) {
- final int offset = -abl.getUpNestedPreScrollRange();
- if (animate) {
- animateOffsetTo(parent, abl, offset, 0);
- } else {
- setHeaderTopBottomOffset(parent, abl, offset);
- }
- } else if ((pendingAction & PENDING_ACTION_EXPANDED) != 0) {
- if (animate) {
- animateOffsetTo(parent, abl, 0, 0);
- } else {
- setHeaderTopBottomOffset(parent, abl, 0);
- }
- }
- }
-
- // Finally reset any pending states
- abl.resetPendingAction();
- mOffsetToChildIndexOnLayout = INVALID_POSITION;
-
- // We may have changed size, so let's constrain the top and bottom offset correctly,
- // just in case we're out of the bounds
- setTopAndBottomOffset(
- MathUtils.clamp(getTopAndBottomOffset(), -abl.getTotalScrollRange(), 0));
-
- // Update the AppBarLayout's drawable state for any elevation changes.
- // This is needed so that the elevation is set in the first layout, so that
- // we don't get a visual elevation jump pre-N (due to the draw dispatch skip)
- updateAppBarLayoutDrawableState(parent, abl, getTopAndBottomOffset(), 0, true);
-
- // Make sure we dispatch the offset update
- abl.dispatchOffsetUpdates(getTopAndBottomOffset());
-
- return handled;
- }
-
- @Override
- boolean canDragView(AppBarLayout view) {
- if (mOnDragCallback != null) {
- // If there is a drag callback set, it's in control
- return mOnDragCallback.canDrag(view);
- }
-
- // Else we'll use the default behaviour of seeing if it can scroll down
- if (mLastNestedScrollingChildRef != null) {
- // If we have a reference to a scrolling view, check it
- final View scrollingView = mLastNestedScrollingChildRef.get();
- return scrollingView != null && scrollingView.isShown()
- && !scrollingView.canScrollVertically(-1);
- } else {
- // Otherwise we assume that the scrolling view hasn't been scrolled and can drag.
- return true;
- }
- }
-
- @Override
- void onFlingFinished(CoordinatorLayout parent, AppBarLayout layout) {
- // At the end of a manual fling, check to see if we need to snap to the edge-child
- snapToChildIfNeeded(parent, layout);
- }
-
- @Override
- int getMaxDragOffset(AppBarLayout view) {
- return -view.getDownNestedScrollRange();
- }
-
- @Override
- int getScrollRangeForDragFling(AppBarLayout view) {
- return view.getTotalScrollRange();
- }
-
- @Override
- int setHeaderTopBottomOffset(CoordinatorLayout coordinatorLayout,
- AppBarLayout appBarLayout, int newOffset, int minOffset, int maxOffset) {
- final int curOffset = getTopBottomOffsetForScrollingSibling();
- int consumed = 0;
-
- if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
- // If we have some scrolling range, and we're currently within the min and max
- // offsets, calculate a new offset
- newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
- if (curOffset != newOffset) {
- final int interpolatedOffset = appBarLayout.hasChildWithInterpolator()
- ? interpolateOffset(appBarLayout, newOffset)
- : newOffset;
-
- final boolean offsetChanged = setTopAndBottomOffset(interpolatedOffset);
-
- // Update how much dy we have consumed
- consumed = curOffset - newOffset;
- // Update the stored sibling offset
- mOffsetDelta = newOffset - interpolatedOffset;
-
- if (!offsetChanged && appBarLayout.hasChildWithInterpolator()) {
- // If the offset hasn't changed and we're using an interpolated scroll
- // then we need to keep any dependent views updated. CoL will do this for
- // us when we move, but we need to do it manually when we don't (as an
- // interpolated scroll may finish early).
- coordinatorLayout.dispatchDependentViewsChanged(appBarLayout);
- }
-
- // Dispatch the updates to any listeners
- appBarLayout.dispatchOffsetUpdates(getTopAndBottomOffset());
-
- // Update the AppBarLayout's drawable state (for any elevation changes)
- updateAppBarLayoutDrawableState(coordinatorLayout, appBarLayout, newOffset,
- newOffset < curOffset ? -1 : 1, false);
- }
- } else {
- // Reset the offset delta
- mOffsetDelta = 0;
- }
-
- return consumed;
- }
-
- @VisibleForTesting
- boolean isOffsetAnimatorRunning() {
- return mOffsetAnimator != null && mOffsetAnimator.isRunning();
- }
-
- private int interpolateOffset(AppBarLayout layout, final int offset) {
- final int absOffset = Math.abs(offset);
-
- for (int i = 0, z = layout.getChildCount(); i < z; i++) {
- final View child = layout.getChildAt(i);
- final AppBarLayout.LayoutParams childLp = (LayoutParams) child.getLayoutParams();
- final Interpolator interpolator = childLp.getScrollInterpolator();
-
- if (absOffset >= child.getTop() && absOffset <= child.getBottom()) {
- if (interpolator != null) {
- int childScrollableHeight = 0;
- final int flags = childLp.getScrollFlags();
- if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
- // We're set to scroll so add the child's height plus margin
- childScrollableHeight += child.getHeight() + childLp.topMargin
- + childLp.bottomMargin;
-
- if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
- // For a collapsing scroll, we to take the collapsed height
- // into account.
- childScrollableHeight -= ViewCompat.getMinimumHeight(child);
- }
- }
-
- if (ViewCompat.getFitsSystemWindows(child)) {
- childScrollableHeight -= layout.getTopInset();
- }
-
- if (childScrollableHeight > 0) {
- final int offsetForView = absOffset - child.getTop();
- final int interpolatedDiff = Math.round(childScrollableHeight *
- interpolator.getInterpolation(
- offsetForView / (float) childScrollableHeight));
-
- return Integer.signum(offset) * (child.getTop() + interpolatedDiff);
- }
- }
-
- // If we get to here then the view on the offset isn't suitable for interpolated
- // scrolling. So break out of the loop
- break;
- }
- }
-
- return offset;
- }
-
- private void updateAppBarLayoutDrawableState(final CoordinatorLayout parent,
- final AppBarLayout layout, final int offset, final int direction,
- final boolean forceJump) {
- final View child = getAppBarChildOnOffset(layout, offset);
- if (child != null) {
- final AppBarLayout.LayoutParams childLp = (LayoutParams) child.getLayoutParams();
- final int flags = childLp.getScrollFlags();
- boolean collapsed = false;
-
- if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
- final int minHeight = ViewCompat.getMinimumHeight(child);
-
- if (direction > 0 && (flags & (LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
- | LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED)) != 0) {
- // We're set to enter always collapsed so we are only collapsed when
- // being scrolled down, and in a collapsed offset
- collapsed = -offset >= child.getBottom() - minHeight - layout.getTopInset();
- } else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
- // We're set to exit until collapsed, so any offset which results in
- // the minimum height (or less) being shown is collapsed
- collapsed = -offset >= child.getBottom() - minHeight - layout.getTopInset();
- }
- }
-
- final boolean changed = layout.setCollapsedState(collapsed);
-
- if (Build.VERSION.SDK_INT >= 11 && (forceJump
- || (changed && shouldJumpElevationState(parent, layout)))) {
- // If the collapsed state changed, we may need to
- // jump to the current state if we have an overlapping view
- layout.jumpDrawablesToCurrentState();
- }
- }
- }
-
- private boolean shouldJumpElevationState(CoordinatorLayout parent, AppBarLayout layout) {
- // We should jump the elevated state if we have a dependent scrolling view which has
- // an overlapping top (i.e. overlaps us)
- final List<View> dependencies = parent.getDependents(layout);
- for (int i = 0, size = dependencies.size(); i < size; i++) {
- final View dependency = dependencies.get(i);
- final CoordinatorLayout.LayoutParams lp =
- (CoordinatorLayout.LayoutParams) dependency.getLayoutParams();
- final CoordinatorLayout.Behavior behavior = lp.getBehavior();
-
- if (behavior instanceof ScrollingViewBehavior) {
- return ((ScrollingViewBehavior) behavior).getOverlayTop() != 0;
- }
- }
- return false;
- }
-
- private static View getAppBarChildOnOffset(final AppBarLayout layout, final int offset) {
- final int absOffset = Math.abs(offset);
- for (int i = 0, z = layout.getChildCount(); i < z; i++) {
- final View child = layout.getChildAt(i);
- if (absOffset >= child.getTop() && absOffset <= child.getBottom()) {
- return child;
- }
- }
- return null;
- }
-
- @Override
- int getTopBottomOffsetForScrollingSibling() {
- return getTopAndBottomOffset() + mOffsetDelta;
- }
-
- @Override
- public Parcelable onSaveInstanceState(CoordinatorLayout parent, AppBarLayout abl) {
- final Parcelable superState = super.onSaveInstanceState(parent, abl);
- final int offset = getTopAndBottomOffset();
-
- // Try and find the first visible child...
- for (int i = 0, count = abl.getChildCount(); i < count; i++) {
- View child = abl.getChildAt(i);
- final int visBottom = child.getBottom() + offset;
-
- if (child.getTop() + offset <= 0 && visBottom >= 0) {
- final SavedState ss = new SavedState(superState);
- ss.firstVisibleChildIndex = i;
- ss.firstVisibleChildAtMinimumHeight =
- visBottom == (ViewCompat.getMinimumHeight(child) + abl.getTopInset());
- ss.firstVisibleChildPercentageShown = visBottom / (float) child.getHeight();
- return ss;
- }
- }
-
- // Else we'll just return the super state
- return superState;
- }
-
- @Override
- public void onRestoreInstanceState(CoordinatorLayout parent, AppBarLayout appBarLayout,
- Parcelable state) {
- if (state instanceof SavedState) {
- final SavedState ss = (SavedState) state;
- super.onRestoreInstanceState(parent, appBarLayout, ss.getSuperState());
- mOffsetToChildIndexOnLayout = ss.firstVisibleChildIndex;
- mOffsetToChildIndexOnLayoutPerc = ss.firstVisibleChildPercentageShown;
- mOffsetToChildIndexOnLayoutIsMinHeight = ss.firstVisibleChildAtMinimumHeight;
- } else {
- super.onRestoreInstanceState(parent, appBarLayout, state);
- mOffsetToChildIndexOnLayout = INVALID_POSITION;
- }
- }
-
- protected static class SavedState extends AbsSavedState {
- int firstVisibleChildIndex;
- float firstVisibleChildPercentageShown;
- boolean firstVisibleChildAtMinimumHeight;
-
- public SavedState(Parcel source, ClassLoader loader) {
- super(source, loader);
- firstVisibleChildIndex = source.readInt();
- firstVisibleChildPercentageShown = source.readFloat();
- firstVisibleChildAtMinimumHeight = source.readByte() != 0;
- }
-
- public SavedState(Parcelable superState) {
- super(superState);
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- super.writeToParcel(dest, flags);
- dest.writeInt(firstVisibleChildIndex);
- dest.writeFloat(firstVisibleChildPercentageShown);
- dest.writeByte((byte) (firstVisibleChildAtMinimumHeight ? 1 : 0));
- }
-
- public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel source, ClassLoader loader) {
- return new SavedState(source, loader);
- }
-
- @Override
- public SavedState createFromParcel(Parcel source) {
- return new SavedState(source, null);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
- }
-
- /**
- * Behavior which should be used by {@link View}s which can scroll vertically and support
- * nested scrolling to automatically scroll any {@link AppBarLayout} siblings.
- */
- public static class ScrollingViewBehavior extends HeaderScrollingViewBehavior {
-
- public ScrollingViewBehavior() {}
-
- public ScrollingViewBehavior(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- final TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.ScrollingViewBehavior_Layout);
- setOverlayTop(a.getDimensionPixelSize(
- R.styleable.ScrollingViewBehavior_Layout_behavior_overlapTop, 0));
- a.recycle();
- }
-
- @Override
- public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
- // We depend on any AppBarLayouts
- return dependency instanceof AppBarLayout;
- }
-
- @Override
- public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
- View dependency) {
- offsetChildAsNeeded(parent, child, dependency);
- return false;
- }
-
- @Override
- public boolean onRequestChildRectangleOnScreen(CoordinatorLayout parent, View child,
- Rect rectangle, boolean immediate) {
- final AppBarLayout header = findFirstDependency(parent.getDependencies(child));
- if (header != null) {
- // Offset the rect by the child's left/top
- rectangle.offset(child.getLeft(), child.getTop());
-
- final Rect parentRect = mTempRect1;
- parentRect.set(0, 0, parent.getWidth(), parent.getHeight());
-
- if (!parentRect.contains(rectangle)) {
- // If the rectangle can not be fully seen the visible bounds, collapse
- // the AppBarLayout
- header.setExpanded(false, !immediate);
- return true;
- }
- }
- return false;
- }
-
- private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {
- final CoordinatorLayout.Behavior behavior =
- ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
- if (behavior instanceof Behavior) {
- // Offset the child, pinning it to the bottom the header-dependency, maintaining
- // any vertical gap and overlap
- final Behavior ablBehavior = (Behavior) behavior;
- ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop())
- + ablBehavior.mOffsetDelta
- + getVerticalLayoutGap()
- - getOverlapPixelsForOffset(dependency));
- }
- }
-
- @Override
- float getOverlapRatioForOffset(final View header) {
- if (header instanceof AppBarLayout) {
- final AppBarLayout abl = (AppBarLayout) header;
- final int totalScrollRange = abl.getTotalScrollRange();
- final int preScrollDown = abl.getDownNestedPreScrollRange();
- final int offset = getAppBarLayoutOffset(abl);
-
- if (preScrollDown != 0 && (totalScrollRange + offset) <= preScrollDown) {
- // If we're in a pre-scroll down. Don't use the offset at all.
- return 0;
- } else {
- final int availScrollRange = totalScrollRange - preScrollDown;
- if (availScrollRange != 0) {
- // Else we'll use a interpolated ratio of the overlap, depending on offset
- return 1f + (offset / (float) availScrollRange);
- }
- }
- }
- return 0f;
- }
-
- private static int getAppBarLayoutOffset(AppBarLayout abl) {
- final CoordinatorLayout.Behavior behavior =
- ((CoordinatorLayout.LayoutParams) abl.getLayoutParams()).getBehavior();
- if (behavior instanceof Behavior) {
- return ((Behavior) behavior).getTopBottomOffsetForScrollingSibling();
- }
- return 0;
- }
-
- @Override
- AppBarLayout findFirstDependency(List<View> views) {
- for (int i = 0, z = views.size(); i < z; i++) {
- View view = views.get(i);
- if (view instanceof AppBarLayout) {
- return (AppBarLayout) view;
- }
- }
- return null;
- }
-
- @Override
- int getScrollRange(View v) {
- if (v instanceof AppBarLayout) {
- return ((AppBarLayout) v).getTotalScrollRange();
- } else {
- return super.getScrollRange(v);
- }
- }
- }
-}
diff --git a/android/support/design/widget/BaseTransientBottomBar.java b/android/support/design/widget/BaseTransientBottomBar.java
deleted file mode 100644
index 18c9ef9..0000000
--- a/android/support/design/widget/BaseTransientBottomBar.java
+++ /dev/null
@@ -1,753 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import static android.support.design.widget.AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.support.annotation.IntDef;
-import android.support.annotation.IntRange;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.design.R;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.WindowInsetsCompat;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.view.accessibility.AccessibilityManager;
-import android.view.animation.Animation;
-import android.view.animation.AnimationUtils;
-import android.widget.FrameLayout;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Base class for lightweight transient bars that are displayed along the bottom edge of the
- * application window.
- *
- * @param <B> The transient bottom bar subclass.
- */
-public abstract class BaseTransientBottomBar<B extends BaseTransientBottomBar<B>> {
- /**
- * Base class for {@link BaseTransientBottomBar} callbacks.
- *
- * @param <B> The transient bottom bar subclass.
- * @see BaseTransientBottomBar#addCallback(BaseCallback)
- */
- public abstract static class BaseCallback<B> {
- /** Indicates that the Snackbar was dismissed via a swipe.*/
- public static final int DISMISS_EVENT_SWIPE = 0;
- /** Indicates that the Snackbar was dismissed via an action click.*/
- public static final int DISMISS_EVENT_ACTION = 1;
- /** Indicates that the Snackbar was dismissed via a timeout.*/
- public static final int DISMISS_EVENT_TIMEOUT = 2;
- /** Indicates that the Snackbar was dismissed via a call to {@link #dismiss()}.*/
- public static final int DISMISS_EVENT_MANUAL = 3;
- /** Indicates that the Snackbar was dismissed from a new Snackbar being shown.*/
- public static final int DISMISS_EVENT_CONSECUTIVE = 4;
-
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef({DISMISS_EVENT_SWIPE, DISMISS_EVENT_ACTION, DISMISS_EVENT_TIMEOUT,
- DISMISS_EVENT_MANUAL, DISMISS_EVENT_CONSECUTIVE})
- @Retention(RetentionPolicy.SOURCE)
- public @interface DismissEvent {}
-
- /**
- * Called when the given {@link BaseTransientBottomBar} has been dismissed, either
- * through a time-out, having been manually dismissed, or an action being clicked.
- *
- * @param transientBottomBar The transient bottom bar which has been dismissed.
- * @param event The event which caused the dismissal. One of either:
- * {@link #DISMISS_EVENT_SWIPE}, {@link #DISMISS_EVENT_ACTION},
- * {@link #DISMISS_EVENT_TIMEOUT}, {@link #DISMISS_EVENT_MANUAL} or
- * {@link #DISMISS_EVENT_CONSECUTIVE}.
- *
- * @see BaseTransientBottomBar#dismiss()
- */
- public void onDismissed(B transientBottomBar, @DismissEvent int event) {
- // empty
- }
-
- /**
- * Called when the given {@link BaseTransientBottomBar} is visible.
- *
- * @param transientBottomBar The transient bottom bar which is now visible.
- * @see BaseTransientBottomBar#show()
- */
- public void onShown(B transientBottomBar) {
- // empty
- }
- }
-
- /**
- * Interface that defines the behavior of the main content of a transient bottom bar.
- */
- public interface ContentViewCallback {
- /**
- * Animates the content of the transient bottom bar in.
- *
- * @param delay Animation delay.
- * @param duration Animation duration.
- */
- void animateContentIn(int delay, int duration);
-
- /**
- * Animates the content of the transient bottom bar out.
- *
- * @param delay Animation delay.
- * @param duration Animation duration.
- */
- void animateContentOut(int delay, int duration);
- }
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef({LENGTH_INDEFINITE, LENGTH_SHORT, LENGTH_LONG})
- @IntRange(from = 1)
- @Retention(RetentionPolicy.SOURCE)
- public @interface Duration {}
-
- /**
- * Show the Snackbar indefinitely. This means that the Snackbar will be displayed from the time
- * that is {@link #show() shown} until either it is dismissed, or another Snackbar is shown.
- *
- * @see #setDuration
- */
- public static final int LENGTH_INDEFINITE = -2;
-
- /**
- * Show the Snackbar for a short period of time.
- *
- * @see #setDuration
- */
- public static final int LENGTH_SHORT = -1;
-
- /**
- * Show the Snackbar for a long period of time.
- *
- * @see #setDuration
- */
- public static final int LENGTH_LONG = 0;
-
- static final int ANIMATION_DURATION = 250;
- static final int ANIMATION_FADE_DURATION = 180;
-
- static final Handler sHandler;
- static final int MSG_SHOW = 0;
- static final int MSG_DISMISS = 1;
-
- // On JB/KK versions of the platform sometimes View.setTranslationY does not
- // result in layout / draw pass, and CoordinatorLayout relies on a draw pass to
- // happen to sync vertical positioning of all its child views
- private static final boolean USE_OFFSET_API = (Build.VERSION.SDK_INT >= 16)
- && (Build.VERSION.SDK_INT <= 19);
-
- static {
- sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
- @Override
- public boolean handleMessage(Message message) {
- switch (message.what) {
- case MSG_SHOW:
- ((BaseTransientBottomBar) message.obj).showView();
- return true;
- case MSG_DISMISS:
- ((BaseTransientBottomBar) message.obj).hideView(message.arg1);
- return true;
- }
- return false;
- }
- });
- }
-
- private final ViewGroup mTargetParent;
- private final Context mContext;
- final SnackbarBaseLayout mView;
- private final ContentViewCallback mContentViewCallback;
- private int mDuration;
-
- private List<BaseCallback<B>> mCallbacks;
-
- private final AccessibilityManager mAccessibilityManager;
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- interface OnLayoutChangeListener {
- void onLayoutChange(View view, int left, int top, int right, int bottom);
- }
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- interface OnAttachStateChangeListener {
- void onViewAttachedToWindow(View v);
- void onViewDetachedFromWindow(View v);
- }
-
- /**
- * Constructor for the transient bottom bar.
- *
- * @param parent The parent for this transient bottom bar.
- * @param content The content view for this transient bottom bar.
- * @param contentViewCallback The content view callback for this transient bottom bar.
- */
- protected BaseTransientBottomBar(@NonNull ViewGroup parent, @NonNull View content,
- @NonNull ContentViewCallback contentViewCallback) {
- if (parent == null) {
- throw new IllegalArgumentException("Transient bottom bar must have non-null parent");
- }
- if (content == null) {
- throw new IllegalArgumentException("Transient bottom bar must have non-null content");
- }
- if (contentViewCallback == null) {
- throw new IllegalArgumentException("Transient bottom bar must have non-null callback");
- }
-
- mTargetParent = parent;
- mContentViewCallback = contentViewCallback;
- mContext = parent.getContext();
-
- ThemeUtils.checkAppCompatTheme(mContext);
-
- LayoutInflater inflater = LayoutInflater.from(mContext);
- // Note that for backwards compatibility reasons we inflate a layout that is defined
- // in the extending Snackbar class. This is to prevent breakage of apps that have custom
- // coordinator layout behaviors that depend on that layout.
- mView = (SnackbarBaseLayout) inflater.inflate(
- R.layout.design_layout_snackbar, mTargetParent, false);
- mView.addView(content);
-
- ViewCompat.setAccessibilityLiveRegion(mView,
- ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);
- ViewCompat.setImportantForAccessibility(mView,
- ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
-
- // Make sure that we fit system windows and have a listener to apply any insets
- ViewCompat.setFitsSystemWindows(mView, true);
- ViewCompat.setOnApplyWindowInsetsListener(mView,
- new android.support.v4.view.OnApplyWindowInsetsListener() {
- @Override
- public WindowInsetsCompat onApplyWindowInsets(View v,
- WindowInsetsCompat insets) {
- // Copy over the bottom inset as padding so that we're displayed
- // above the navigation bar
- v.setPadding(v.getPaddingLeft(), v.getPaddingTop(),
- v.getPaddingRight(), insets.getSystemWindowInsetBottom());
- return insets;
- }
- });
-
- mAccessibilityManager = (AccessibilityManager)
- mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
- }
-
- /**
- * Set how long to show the view for.
- *
- * @param duration either be one of the predefined lengths:
- * {@link #LENGTH_SHORT}, {@link #LENGTH_LONG}, or a custom duration
- * in milliseconds.
- */
- @NonNull
- public B setDuration(@Duration int duration) {
- mDuration = duration;
- return (B) this;
- }
-
- /**
- * Return the duration.
- *
- * @see #setDuration
- */
- @Duration
- public int getDuration() {
- return mDuration;
- }
-
- /**
- * Returns the {@link BaseTransientBottomBar}'s context.
- */
- @NonNull
- public Context getContext() {
- return mContext;
- }
-
- /**
- * Returns the {@link BaseTransientBottomBar}'s view.
- */
- @NonNull
- public View getView() {
- return mView;
- }
-
- /**
- * Show the {@link BaseTransientBottomBar}.
- */
- public void show() {
- SnackbarManager.getInstance().show(mDuration, mManagerCallback);
- }
-
- /**
- * Dismiss the {@link BaseTransientBottomBar}.
- */
- public void dismiss() {
- dispatchDismiss(BaseCallback.DISMISS_EVENT_MANUAL);
- }
-
- void dispatchDismiss(@BaseCallback.DismissEvent int event) {
- SnackbarManager.getInstance().dismiss(mManagerCallback, event);
- }
-
- /**
- * Adds the specified callback to the list of callbacks that will be notified of transient
- * bottom bar events.
- *
- * @param callback Callback to notify when transient bottom bar events occur.
- * @see #removeCallback(BaseCallback)
- */
- @NonNull
- public B addCallback(@NonNull BaseCallback<B> callback) {
- if (callback == null) {
- return (B) this;
- }
- if (mCallbacks == null) {
- mCallbacks = new ArrayList<BaseCallback<B>>();
- }
- mCallbacks.add(callback);
- return (B) this;
- }
-
- /**
- * Removes the specified callback from the list of callbacks that will be notified of transient
- * bottom bar events.
- *
- * @param callback Callback to remove from being notified of transient bottom bar events
- * @see #addCallback(BaseCallback)
- */
- @NonNull
- public B removeCallback(@NonNull BaseCallback<B> callback) {
- if (callback == null) {
- return (B) this;
- }
- if (mCallbacks == null) {
- // This can happen if this method is called before the first call to addCallback
- return (B) this;
- }
- mCallbacks.remove(callback);
- return (B) this;
- }
-
- /**
- * Return whether this {@link BaseTransientBottomBar} is currently being shown.
- */
- public boolean isShown() {
- return SnackbarManager.getInstance().isCurrent(mManagerCallback);
- }
-
- /**
- * Returns whether this {@link BaseTransientBottomBar} is currently being shown, or is queued
- * to be shown next.
- */
- public boolean isShownOrQueued() {
- return SnackbarManager.getInstance().isCurrentOrNext(mManagerCallback);
- }
-
- final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
- @Override
- public void show() {
- sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, BaseTransientBottomBar.this));
- }
-
- @Override
- public void dismiss(int event) {
- sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0,
- BaseTransientBottomBar.this));
- }
- };
-
- final void showView() {
- if (mView.getParent() == null) {
- final ViewGroup.LayoutParams lp = mView.getLayoutParams();
-
- if (lp instanceof CoordinatorLayout.LayoutParams) {
- // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior
- final CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp;
-
- final Behavior behavior = new Behavior();
- behavior.setStartAlphaSwipeDistance(0.1f);
- behavior.setEndAlphaSwipeDistance(0.6f);
- behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
- behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
- @Override
- public void onDismiss(View view) {
- view.setVisibility(View.GONE);
- dispatchDismiss(BaseCallback.DISMISS_EVENT_SWIPE);
- }
-
- @Override
- public void onDragStateChanged(int state) {
- switch (state) {
- case SwipeDismissBehavior.STATE_DRAGGING:
- case SwipeDismissBehavior.STATE_SETTLING:
- // If the view is being dragged or settling, pause the timeout
- SnackbarManager.getInstance().pauseTimeout(mManagerCallback);
- break;
- case SwipeDismissBehavior.STATE_IDLE:
- // If the view has been released and is idle, restore the timeout
- SnackbarManager.getInstance()
- .restoreTimeoutIfPaused(mManagerCallback);
- break;
- }
- }
- });
- clp.setBehavior(behavior);
- // Also set the inset edge so that views can dodge the bar correctly
- clp.insetEdge = Gravity.BOTTOM;
- }
-
- mTargetParent.addView(mView);
- }
-
- mView.setOnAttachStateChangeListener(
- new BaseTransientBottomBar.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(View v) {}
-
- @Override
- public void onViewDetachedFromWindow(View v) {
- if (isShownOrQueued()) {
- // If we haven't already been dismissed then this event is coming from a
- // non-user initiated action. Hence we need to make sure that we callback
- // and keep our state up to date. We need to post the call since
- // removeView() will call through to onDetachedFromWindow and thus overflow.
- sHandler.post(new Runnable() {
- @Override
- public void run() {
- onViewHidden(BaseCallback.DISMISS_EVENT_MANUAL);
- }
- });
- }
- }
- });
-
- if (ViewCompat.isLaidOut(mView)) {
- if (shouldAnimate()) {
- // If animations are enabled, animate it in
- animateViewIn();
- } else {
- // Else if anims are disabled just call back now
- onViewShown();
- }
- } else {
- // Otherwise, add one of our layout change listeners and show it in when laid out
- mView.setOnLayoutChangeListener(new BaseTransientBottomBar.OnLayoutChangeListener() {
- @Override
- public void onLayoutChange(View view, int left, int top, int right, int bottom) {
- mView.setOnLayoutChangeListener(null);
-
- if (shouldAnimate()) {
- // If animations are enabled, animate it in
- animateViewIn();
- } else {
- // Else if anims are disabled just call back now
- onViewShown();
- }
- }
- });
- }
- }
-
- void animateViewIn() {
- if (Build.VERSION.SDK_INT >= 12) {
- final int viewHeight = mView.getHeight();
- if (USE_OFFSET_API) {
- ViewCompat.offsetTopAndBottom(mView, viewHeight);
- } else {
- mView.setTranslationY(viewHeight);
- }
- final ValueAnimator animator = new ValueAnimator();
- animator.setIntValues(viewHeight, 0);
- animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
- animator.setDuration(ANIMATION_DURATION);
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animator) {
- mContentViewCallback.animateContentIn(
- ANIMATION_DURATION - ANIMATION_FADE_DURATION,
- ANIMATION_FADE_DURATION);
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- onViewShown();
- }
- });
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- private int mPreviousAnimatedIntValue = viewHeight;
-
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- int currentAnimatedIntValue = (int) animator.getAnimatedValue();
- if (USE_OFFSET_API) {
- ViewCompat.offsetTopAndBottom(mView,
- currentAnimatedIntValue - mPreviousAnimatedIntValue);
- } else {
- mView.setTranslationY(currentAnimatedIntValue);
- }
- mPreviousAnimatedIntValue = currentAnimatedIntValue;
- }
- });
- animator.start();
- } else {
- final Animation anim = AnimationUtils.loadAnimation(mView.getContext(),
- R.anim.design_snackbar_in);
- anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
- anim.setDuration(ANIMATION_DURATION);
- anim.setAnimationListener(new Animation.AnimationListener() {
- @Override
- public void onAnimationEnd(Animation animation) {
- onViewShown();
- }
-
- @Override
- public void onAnimationStart(Animation animation) {}
-
- @Override
- public void onAnimationRepeat(Animation animation) {}
- });
- mView.startAnimation(anim);
- }
- }
-
- private void animateViewOut(final int event) {
- if (Build.VERSION.SDK_INT >= 12) {
- final ValueAnimator animator = new ValueAnimator();
- animator.setIntValues(0, mView.getHeight());
- animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
- animator.setDuration(ANIMATION_DURATION);
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animator) {
- mContentViewCallback.animateContentOut(0, ANIMATION_FADE_DURATION);
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- onViewHidden(event);
- }
- });
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- private int mPreviousAnimatedIntValue = 0;
-
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- int currentAnimatedIntValue = (int) animator.getAnimatedValue();
- if (USE_OFFSET_API) {
- ViewCompat.offsetTopAndBottom(mView,
- currentAnimatedIntValue - mPreviousAnimatedIntValue);
- } else {
- mView.setTranslationY(currentAnimatedIntValue);
- }
- mPreviousAnimatedIntValue = currentAnimatedIntValue;
- }
- });
- animator.start();
- } else {
- final Animation anim = AnimationUtils.loadAnimation(mView.getContext(),
- R.anim.design_snackbar_out);
- anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
- anim.setDuration(ANIMATION_DURATION);
- anim.setAnimationListener(new Animation.AnimationListener() {
- @Override
- public void onAnimationEnd(Animation animation) {
- onViewHidden(event);
- }
-
- @Override
- public void onAnimationStart(Animation animation) {}
-
- @Override
- public void onAnimationRepeat(Animation animation) {}
- });
- mView.startAnimation(anim);
- }
- }
-
- final void hideView(@BaseCallback.DismissEvent final int event) {
- if (shouldAnimate() && mView.getVisibility() == View.VISIBLE) {
- animateViewOut(event);
- } else {
- // If anims are disabled or the view isn't visible, just call back now
- onViewHidden(event);
- }
- }
-
- void onViewShown() {
- SnackbarManager.getInstance().onShown(mManagerCallback);
- if (mCallbacks != null) {
- // Notify the callbacks. Do that from the end of the list so that if a callback
- // removes itself as the result of being called, it won't mess up with our iteration
- int callbackCount = mCallbacks.size();
- for (int i = callbackCount - 1; i >= 0; i--) {
- mCallbacks.get(i).onShown((B) this);
- }
- }
- }
-
- void onViewHidden(int event) {
- // First tell the SnackbarManager that it has been dismissed
- SnackbarManager.getInstance().onDismissed(mManagerCallback);
- if (mCallbacks != null) {
- // Notify the callbacks. Do that from the end of the list so that if a callback
- // removes itself as the result of being called, it won't mess up with our iteration
- int callbackCount = mCallbacks.size();
- for (int i = callbackCount - 1; i >= 0; i--) {
- mCallbacks.get(i).onDismissed((B) this, event);
- }
- }
- if (Build.VERSION.SDK_INT < 11) {
- // We need to hide the Snackbar on pre-v11 since it uses an old style Animation.
- // ViewGroup has special handling in removeView() when getAnimation() != null in
- // that it waits. This then means that the calculated insets are wrong and the
- // any dodging views do not return. We workaround it by setting the view to gone while
- // ViewGroup actually gets around to removing it.
- mView.setVisibility(View.GONE);
- }
- // Lastly, hide and remove the view from the parent (if attached)
- final ViewParent parent = mView.getParent();
- if (parent instanceof ViewGroup) {
- ((ViewGroup) parent).removeView(mView);
- }
- }
-
- /**
- * Returns true if we should animate the Snackbar view in/out.
- */
- boolean shouldAnimate() {
- return !mAccessibilityManager.isEnabled();
- }
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- static class SnackbarBaseLayout extends FrameLayout {
- private BaseTransientBottomBar.OnLayoutChangeListener mOnLayoutChangeListener;
- private BaseTransientBottomBar.OnAttachStateChangeListener mOnAttachStateChangeListener;
-
- SnackbarBaseLayout(Context context) {
- this(context, null);
- }
-
- SnackbarBaseLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout);
- if (a.hasValue(R.styleable.SnackbarLayout_elevation)) {
- ViewCompat.setElevation(this, a.getDimensionPixelSize(
- R.styleable.SnackbarLayout_elevation, 0));
- }
- a.recycle();
-
- setClickable(true);
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
- if (mOnLayoutChangeListener != null) {
- mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b);
- }
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- if (mOnAttachStateChangeListener != null) {
- mOnAttachStateChangeListener.onViewAttachedToWindow(this);
- }
-
- ViewCompat.requestApplyInsets(this);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- if (mOnAttachStateChangeListener != null) {
- mOnAttachStateChangeListener.onViewDetachedFromWindow(this);
- }
- }
-
- void setOnLayoutChangeListener(
- BaseTransientBottomBar.OnLayoutChangeListener onLayoutChangeListener) {
- mOnLayoutChangeListener = onLayoutChangeListener;
- }
-
- void setOnAttachStateChangeListener(
- BaseTransientBottomBar.OnAttachStateChangeListener listener) {
- mOnAttachStateChangeListener = listener;
- }
- }
-
- final class Behavior extends SwipeDismissBehavior<SnackbarBaseLayout> {
- @Override
- public boolean canSwipeDismissView(View child) {
- return child instanceof SnackbarBaseLayout;
- }
-
- @Override
- public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarBaseLayout child,
- MotionEvent event) {
- switch (event.getActionMasked()) {
- case MotionEvent.ACTION_DOWN:
- // We want to make sure that we disable any Snackbar timeouts if the user is
- // currently touching the Snackbar. We restore the timeout when complete
- if (parent.isPointInChildBounds(child, (int) event.getX(),
- (int) event.getY())) {
- SnackbarManager.getInstance().pauseTimeout(mManagerCallback);
- }
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- SnackbarManager.getInstance().restoreTimeoutIfPaused(mManagerCallback);
- break;
- }
- return super.onInterceptTouchEvent(parent, child, event);
- }
- }
-}
diff --git a/android/support/design/widget/BottomNavigationView.java b/android/support/design/widget/BottomNavigationView.java
deleted file mode 100644
index 61dba87..0000000
--- a/android/support/design/widget/BottomNavigationView.java
+++ /dev/null
@@ -1,477 +0,0 @@
-/*
- * 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.support.design.widget;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.IdRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.design.R;
-import android.support.design.internal.BottomNavigationMenu;
-import android.support.design.internal.BottomNavigationMenuView;
-import android.support.design.internal.BottomNavigationPresenter;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.view.AbsSavedState;
-import android.support.v4.view.ViewCompat;
-import android.support.v7.content.res.AppCompatResources;
-import android.support.v7.view.SupportMenuInflater;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.widget.TintTypedArray;
-import android.util.AttributeSet;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-
-/**
- * <p>
- * Represents a standard bottom navigation bar for application. It is an implementation of
- * <a href="https://material.google.com/components/bottom-navigation.html">material design bottom
- * navigation</a>.
- * </p>
- *
- * <p>
- * Bottom navigation bars make it easy for users to explore and switch between top-level views in
- * a single tap. It should be used when application has three to five top-level destinations.
- * </p>
- *
- * <p>
- * The bar contents can be populated by specifying a menu resource file. Each menu item title, icon
- * and enabled state will be used for displaying bottom navigation bar items. Menu items can also be
- * used for programmatically selecting which destination is currently active. It can be done using
- * {@code MenuItem#setChecked(true)}
- * </p>
- *
- * <pre>
- * layout resource file:
- * <android.support.design.widget.BottomNavigationView
- * xmlns:android="http://schemas.android.com/apk/res/android"
- * xmlns:app="http://schemas.android.com/apk/res-auto"
- * android:id="@+id/navigation"
- * android:layout_width="match_parent"
- * android:layout_height="56dp"
- * android:layout_gravity="start"
- * app:menu="@menu/my_navigation_items" />
- *
- * res/menu/my_navigation_items.xml:
- * <menu xmlns:android="http://schemas.android.com/apk/res/android">
- * <item android:id="@+id/action_search"
- * android:title="@string/menu_search"
- * android:icon="@drawable/ic_search" />
- * <item android:id="@+id/action_settings"
- * android:title="@string/menu_settings"
- * android:icon="@drawable/ic_add" />
- * <item android:id="@+id/action_navigation"
- * android:title="@string/menu_navigation"
- * android:icon="@drawable/ic_action_navigation_menu" />
- * </menu>
- * </pre>
- */
-public class BottomNavigationView extends FrameLayout {
-
- private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
- private static final int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled};
-
- private static final int MENU_PRESENTER_ID = 1;
-
- private final MenuBuilder mMenu;
- private final BottomNavigationMenuView mMenuView;
- private final BottomNavigationPresenter mPresenter = new BottomNavigationPresenter();
- private MenuInflater mMenuInflater;
-
- private OnNavigationItemSelectedListener mSelectedListener;
- private OnNavigationItemReselectedListener mReselectedListener;
-
- public BottomNavigationView(Context context) {
- this(context, null);
- }
-
- public BottomNavigationView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public BottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- ThemeUtils.checkAppCompatTheme(context);
-
- // Create the menu
- mMenu = new BottomNavigationMenu(context);
-
- mMenuView = new BottomNavigationMenuView(context);
- FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
- params.gravity = Gravity.CENTER;
- mMenuView.setLayoutParams(params);
-
- mPresenter.setBottomNavigationMenuView(mMenuView);
- mPresenter.setId(MENU_PRESENTER_ID);
- mMenuView.setPresenter(mPresenter);
- mMenu.addMenuPresenter(mPresenter);
- mPresenter.initForMenu(getContext(), mMenu);
-
- // Custom attributes
- TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
- R.styleable.BottomNavigationView, defStyleAttr,
- R.style.Widget_Design_BottomNavigationView);
-
- if (a.hasValue(R.styleable.BottomNavigationView_itemIconTint)) {
- mMenuView.setIconTintList(
- a.getColorStateList(R.styleable.BottomNavigationView_itemIconTint));
- } else {
- mMenuView.setIconTintList(
- createDefaultColorStateList(android.R.attr.textColorSecondary));
- }
- if (a.hasValue(R.styleable.BottomNavigationView_itemTextColor)) {
- mMenuView.setItemTextColor(
- a.getColorStateList(R.styleable.BottomNavigationView_itemTextColor));
- } else {
- mMenuView.setItemTextColor(
- createDefaultColorStateList(android.R.attr.textColorSecondary));
- }
- if (a.hasValue(R.styleable.BottomNavigationView_elevation)) {
- ViewCompat.setElevation(this, a.getDimensionPixelSize(
- R.styleable.BottomNavigationView_elevation, 0));
- }
-
- int itemBackground = a.getResourceId(R.styleable.BottomNavigationView_itemBackground, 0);
- mMenuView.setItemBackgroundRes(itemBackground);
-
- if (a.hasValue(R.styleable.BottomNavigationView_menu)) {
- inflateMenu(a.getResourceId(R.styleable.BottomNavigationView_menu, 0));
- }
- a.recycle();
-
- addView(mMenuView, params);
- if (Build.VERSION.SDK_INT < 21) {
- addCompatibilityTopDivider(context);
- }
-
- mMenu.setCallback(new MenuBuilder.Callback() {
- @Override
- public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
- if (mReselectedListener != null && item.getItemId() == getSelectedItemId()) {
- mReselectedListener.onNavigationItemReselected(item);
- return true; // item is already selected
- }
- return mSelectedListener != null
- && !mSelectedListener.onNavigationItemSelected(item);
- }
-
- @Override
- public void onMenuModeChange(MenuBuilder menu) {}
- });
- }
-
- /**
- * Set a listener that will be notified when a bottom navigation item is selected. This listener
- * will also be notified when the currently selected item is reselected, unless an
- * {@link OnNavigationItemReselectedListener} has also been set.
- *
- * @param listener The listener to notify
- *
- * @see #setOnNavigationItemReselectedListener(OnNavigationItemReselectedListener)
- */
- public void setOnNavigationItemSelectedListener(
- @Nullable OnNavigationItemSelectedListener listener) {
- mSelectedListener = listener;
- }
-
- /**
- * Set a listener that will be notified when the currently selected bottom navigation item is
- * reselected. This does not require an {@link OnNavigationItemSelectedListener} to be set.
- *
- * @param listener The listener to notify
- *
- * @see #setOnNavigationItemSelectedListener(OnNavigationItemSelectedListener)
- */
- public void setOnNavigationItemReselectedListener(
- @Nullable OnNavigationItemReselectedListener listener) {
- mReselectedListener = listener;
- }
-
- /**
- * Returns the {@link Menu} instance associated with this bottom navigation bar.
- */
- @NonNull
- public Menu getMenu() {
- return mMenu;
- }
-
- /**
- * Inflate a menu resource into this navigation view.
- *
- * <p>Existing items in the menu will not be modified or removed.</p>
- *
- * @param resId ID of a menu resource to inflate
- */
- public void inflateMenu(int resId) {
- mPresenter.setUpdateSuspended(true);
- getMenuInflater().inflate(resId, mMenu);
- mPresenter.setUpdateSuspended(false);
- mPresenter.updateMenuView(true);
- }
-
- /**
- * @return The maximum number of items that can be shown in BottomNavigationView.
- */
- public int getMaxItemCount() {
- return BottomNavigationMenu.MAX_ITEM_COUNT;
- }
-
- /**
- * Returns the tint which is applied to our menu items' icons.
- *
- * @see #setItemIconTintList(ColorStateList)
- *
- * @attr ref R.styleable#BottomNavigationView_itemIconTint
- */
- @Nullable
- public ColorStateList getItemIconTintList() {
- return mMenuView.getIconTintList();
- }
-
- /**
- * Set the tint which is applied to our menu items' icons.
- *
- * @param tint the tint to apply.
- *
- * @attr ref R.styleable#BottomNavigationView_itemIconTint
- */
- public void setItemIconTintList(@Nullable ColorStateList tint) {
- mMenuView.setIconTintList(tint);
- }
-
- /**
- * Returns colors used for the different states (normal, selected, focused, etc.) of the menu
- * item text.
- *
- * @see #setItemTextColor(ColorStateList)
- *
- * @return the ColorStateList of colors used for the different states of the menu items text.
- *
- * @attr ref R.styleable#BottomNavigationView_itemTextColor
- */
- @Nullable
- public ColorStateList getItemTextColor() {
- return mMenuView.getItemTextColor();
- }
-
- /**
- * Set the colors to use for the different states (normal, selected, focused, etc.) of the menu
- * item text.
- *
- * @see #getItemTextColor()
- *
- * @attr ref R.styleable#BottomNavigationView_itemTextColor
- */
- public void setItemTextColor(@Nullable ColorStateList textColor) {
- mMenuView.setItemTextColor(textColor);
- }
-
- /**
- * Returns the background resource of the menu items.
- *
- * @see #setItemBackgroundResource(int)
- *
- * @attr ref R.styleable#BottomNavigationView_itemBackground
- */
- @DrawableRes
- public int getItemBackgroundResource() {
- return mMenuView.getItemBackgroundRes();
- }
-
- /**
- * Set the background of our menu items to the given resource.
- *
- * @param resId The identifier of the resource.
- *
- * @attr ref R.styleable#BottomNavigationView_itemBackground
- */
- public void setItemBackgroundResource(@DrawableRes int resId) {
- mMenuView.setItemBackgroundRes(resId);
- }
-
- /**
- * Returns the currently selected menu item ID, or zero if there is no menu.
- *
- * @see #setSelectedItemId(int)
- */
- @IdRes
- public int getSelectedItemId() {
- return mMenuView.getSelectedItemId();
- }
-
- /**
- * Set the selected menu item ID. This behaves the same as tapping on an item.
- *
- * @param itemId The menu item ID. If no item has this ID, the current selection is unchanged.
- *
- * @see #getSelectedItemId()
- */
- public void setSelectedItemId(@IdRes int itemId) {
- MenuItem item = mMenu.findItem(itemId);
- if (item != null) {
- if (!mMenu.performItemAction(item, mPresenter, 0)) {
- item.setChecked(true);
- }
- }
- }
-
- /**
- * Listener for handling selection events on bottom navigation items.
- */
- public interface OnNavigationItemSelectedListener {
-
- /**
- * Called when an item in the bottom navigation menu is selected.
- *
- * @param item The selected item
- *
- * @return true to display the item as the selected item and false if the item should not
- * be selected. Consider setting non-selectable items as disabled preemptively to
- * make them appear non-interactive.
- */
- boolean onNavigationItemSelected(@NonNull MenuItem item);
- }
-
- /**
- * Listener for handling reselection events on bottom navigation items.
- */
- public interface OnNavigationItemReselectedListener {
-
- /**
- * Called when the currently selected item in the bottom navigation menu is selected again.
- *
- * @param item The selected item
- */
- void onNavigationItemReselected(@NonNull MenuItem item);
- }
-
- private void addCompatibilityTopDivider(Context context) {
- View divider = new View(context);
- divider.setBackgroundColor(
- ContextCompat.getColor(context, R.color.design_bottom_navigation_shadow_color));
- FrameLayout.LayoutParams dividerParams = new FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- getResources().getDimensionPixelSize(
- R.dimen.design_bottom_navigation_shadow_height));
- divider.setLayoutParams(dividerParams);
- addView(divider);
- }
-
- private MenuInflater getMenuInflater() {
- if (mMenuInflater == null) {
- mMenuInflater = new SupportMenuInflater(getContext());
- }
- return mMenuInflater;
- }
-
- private ColorStateList createDefaultColorStateList(int baseColorThemeAttr) {
- final TypedValue value = new TypedValue();
- if (!getContext().getTheme().resolveAttribute(baseColorThemeAttr, value, true)) {
- return null;
- }
- ColorStateList baseColor = AppCompatResources.getColorStateList(
- getContext(), value.resourceId);
- if (!getContext().getTheme().resolveAttribute(
- android.support.v7.appcompat.R.attr.colorPrimary, value, true)) {
- return null;
- }
- int colorPrimary = value.data;
- int defaultColor = baseColor.getDefaultColor();
- return new ColorStateList(new int[][]{
- DISABLED_STATE_SET,
- CHECKED_STATE_SET,
- EMPTY_STATE_SET
- }, new int[]{
- baseColor.getColorForState(DISABLED_STATE_SET, defaultColor),
- colorPrimary,
- defaultColor
- });
- }
-
- @Override
- protected Parcelable onSaveInstanceState() {
- Parcelable superState = super.onSaveInstanceState();
- SavedState savedState = new SavedState(superState);
- savedState.menuPresenterState = new Bundle();
- mMenu.savePresenterStates(savedState.menuPresenterState);
- return savedState;
- }
-
- @Override
- protected void onRestoreInstanceState(Parcelable state) {
- if (!(state instanceof SavedState)) {
- super.onRestoreInstanceState(state);
- return;
- }
- SavedState savedState = (SavedState) state;
- super.onRestoreInstanceState(savedState.getSuperState());
- mMenu.restorePresenterStates(savedState.menuPresenterState);
- }
-
- static class SavedState extends AbsSavedState {
- Bundle menuPresenterState;
-
- public SavedState(Parcelable superState) {
- super(superState);
- }
-
- public SavedState(Parcel source, ClassLoader loader) {
- super(source, loader);
- readFromParcel(source, loader);
- }
-
- @Override
- public void writeToParcel(@NonNull Parcel out, int flags) {
- super.writeToParcel(out, flags);
- out.writeBundle(menuPresenterState);
- }
-
- private void readFromParcel(Parcel in, ClassLoader loader) {
- menuPresenterState = in.readBundle(loader);
- }
-
- public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in, ClassLoader loader) {
- return new SavedState(in, loader);
- }
-
- @Override
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in, null);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
-}
diff --git a/android/support/design/widget/BottomSheetBehavior.java b/android/support/design/widget/BottomSheetBehavior.java
deleted file mode 100644
index 00ce8f9..0000000
--- a/android/support/design/widget/BottomSheetBehavior.java
+++ /dev/null
@@ -1,829 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.VisibleForTesting;
-import android.support.design.R;
-import android.support.v4.math.MathUtils;
-import android.support.v4.view.AbsSavedState;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.widget.ViewDragHelper;
-import android.util.AttributeSet;
-import android.util.TypedValue;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.ref.WeakReference;
-
-
-/**
- * An interaction behavior plugin for a child view of {@link CoordinatorLayout} to make it work as
- * a bottom sheet.
- */
-public class BottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
-
- /**
- * Callback for monitoring events about bottom sheets.
- */
- public abstract static class BottomSheetCallback {
-
- /**
- * Called when the bottom sheet changes its state.
- *
- * @param bottomSheet The bottom sheet view.
- * @param newState The new state. This will be one of {@link #STATE_DRAGGING},
- * {@link #STATE_SETTLING}, {@link #STATE_EXPANDED},
- * {@link #STATE_COLLAPSED}, or {@link #STATE_HIDDEN}.
- */
- public abstract void onStateChanged(@NonNull View bottomSheet, @State int newState);
-
- /**
- * Called when the bottom sheet is being dragged.
- *
- * @param bottomSheet The bottom sheet view.
- * @param slideOffset The new offset of this bottom sheet within [-1,1] range. Offset
- * increases as this bottom sheet is moving upward. From 0 to 1 the sheet
- * is between collapsed and expanded states and from -1 to 0 it is
- * between hidden and collapsed states.
- */
- public abstract void onSlide(@NonNull View bottomSheet, float slideOffset);
- }
-
- /**
- * The bottom sheet is dragging.
- */
- public static final int STATE_DRAGGING = 1;
-
- /**
- * The bottom sheet is settling.
- */
- public static final int STATE_SETTLING = 2;
-
- /**
- * The bottom sheet is expanded.
- */
- public static final int STATE_EXPANDED = 3;
-
- /**
- * The bottom sheet is collapsed.
- */
- public static final int STATE_COLLAPSED = 4;
-
- /**
- * The bottom sheet is hidden.
- */
- public static final int STATE_HIDDEN = 5;
-
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef({STATE_EXPANDED, STATE_COLLAPSED, STATE_DRAGGING, STATE_SETTLING, STATE_HIDDEN})
- @Retention(RetentionPolicy.SOURCE)
- public @interface State {}
-
- /**
- * Peek at the 16:9 ratio keyline of its parent.
- *
- * <p>This can be used as a parameter for {@link #setPeekHeight(int)}.
- * {@link #getPeekHeight()} will return this when the value is set.</p>
- */
- public static final int PEEK_HEIGHT_AUTO = -1;
-
- private static final float HIDE_THRESHOLD = 0.5f;
-
- private static final float HIDE_FRICTION = 0.1f;
-
- private float mMaximumVelocity;
-
- private int mPeekHeight;
-
- private boolean mPeekHeightAuto;
-
- private int mPeekHeightMin;
-
- int mMinOffset;
-
- int mMaxOffset;
-
- boolean mHideable;
-
- private boolean mSkipCollapsed;
-
- @State
- int mState = STATE_COLLAPSED;
-
- ViewDragHelper mViewDragHelper;
-
- private boolean mIgnoreEvents;
-
- private int mLastNestedScrollDy;
-
- private boolean mNestedScrolled;
-
- int mParentHeight;
-
- WeakReference<V> mViewRef;
-
- WeakReference<View> mNestedScrollingChildRef;
-
- private BottomSheetCallback mCallback;
-
- private VelocityTracker mVelocityTracker;
-
- int mActivePointerId;
-
- private int mInitialY;
-
- boolean mTouchingScrollingChild;
-
- /**
- * Default constructor for instantiating BottomSheetBehaviors.
- */
- public BottomSheetBehavior() {
- }
-
- /**
- * Default constructor for inflating BottomSheetBehaviors from layout.
- *
- * @param context The {@link Context}.
- * @param attrs The {@link AttributeSet}.
- */
- public BottomSheetBehavior(Context context, AttributeSet attrs) {
- super(context, attrs);
- TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.BottomSheetBehavior_Layout);
- TypedValue value = a.peekValue(R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight);
- if (value != null && value.data == PEEK_HEIGHT_AUTO) {
- setPeekHeight(value.data);
- } else {
- setPeekHeight(a.getDimensionPixelSize(
- R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight, PEEK_HEIGHT_AUTO));
- }
- setHideable(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_hideable, false));
- setSkipCollapsed(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed,
- false));
- a.recycle();
- ViewConfiguration configuration = ViewConfiguration.get(context);
- mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
- }
-
- @Override
- public Parcelable onSaveInstanceState(CoordinatorLayout parent, V child) {
- return new SavedState(super.onSaveInstanceState(parent, child), mState);
- }
-
- @Override
- public void onRestoreInstanceState(CoordinatorLayout parent, V child, Parcelable state) {
- SavedState ss = (SavedState) state;
- super.onRestoreInstanceState(parent, child, ss.getSuperState());
- // Intermediate states are restored as collapsed state
- if (ss.state == STATE_DRAGGING || ss.state == STATE_SETTLING) {
- mState = STATE_COLLAPSED;
- } else {
- mState = ss.state;
- }
- }
-
- @Override
- public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
- if (ViewCompat.getFitsSystemWindows(parent) && !ViewCompat.getFitsSystemWindows(child)) {
- ViewCompat.setFitsSystemWindows(child, true);
- }
- int savedTop = child.getTop();
- // First let the parent lay it out
- parent.onLayoutChild(child, layoutDirection);
- // Offset the bottom sheet
- mParentHeight = parent.getHeight();
- int peekHeight;
- if (mPeekHeightAuto) {
- if (mPeekHeightMin == 0) {
- mPeekHeightMin = parent.getResources().getDimensionPixelSize(
- R.dimen.design_bottom_sheet_peek_height_min);
- }
- peekHeight = Math.max(mPeekHeightMin, mParentHeight - parent.getWidth() * 9 / 16);
- } else {
- peekHeight = mPeekHeight;
- }
- mMinOffset = Math.max(0, mParentHeight - child.getHeight());
- mMaxOffset = Math.max(mParentHeight - peekHeight, mMinOffset);
- if (mState == STATE_EXPANDED) {
- ViewCompat.offsetTopAndBottom(child, mMinOffset);
- } else if (mHideable && mState == STATE_HIDDEN) {
- ViewCompat.offsetTopAndBottom(child, mParentHeight);
- } else if (mState == STATE_COLLAPSED) {
- ViewCompat.offsetTopAndBottom(child, mMaxOffset);
- } else if (mState == STATE_DRAGGING || mState == STATE_SETTLING) {
- ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop());
- }
- if (mViewDragHelper == null) {
- mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);
- }
- mViewRef = new WeakReference<>(child);
- mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
- return true;
- }
-
- @Override
- public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
- if (!child.isShown()) {
- mIgnoreEvents = true;
- return false;
- }
- int action = event.getActionMasked();
- // Record the velocity
- if (action == MotionEvent.ACTION_DOWN) {
- reset();
- }
- if (mVelocityTracker == null) {
- mVelocityTracker = VelocityTracker.obtain();
- }
- mVelocityTracker.addMovement(event);
- switch (action) {
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- mTouchingScrollingChild = false;
- mActivePointerId = MotionEvent.INVALID_POINTER_ID;
- // Reset the ignore flag
- if (mIgnoreEvents) {
- mIgnoreEvents = false;
- return false;
- }
- break;
- case MotionEvent.ACTION_DOWN:
- int initialX = (int) event.getX();
- mInitialY = (int) event.getY();
- View scroll = mNestedScrollingChildRef != null
- ? mNestedScrollingChildRef.get() : null;
- if (scroll != null && parent.isPointInChildBounds(scroll, initialX, mInitialY)) {
- mActivePointerId = event.getPointerId(event.getActionIndex());
- mTouchingScrollingChild = true;
- }
- mIgnoreEvents = mActivePointerId == MotionEvent.INVALID_POINTER_ID &&
- !parent.isPointInChildBounds(child, initialX, mInitialY);
- break;
- }
- if (!mIgnoreEvents && mViewDragHelper.shouldInterceptTouchEvent(event)) {
- return true;
- }
- // We have to handle cases that the ViewDragHelper does not capture the bottom sheet because
- // it is not the top most view of its parent. This is not necessary when the touch event is
- // happening over the scrolling content as nested scrolling logic handles that case.
- View scroll = mNestedScrollingChildRef.get();
- return action == MotionEvent.ACTION_MOVE && scroll != null &&
- !mIgnoreEvents && mState != STATE_DRAGGING &&
- !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) &&
- Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop();
- }
-
- @Override
- public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
- if (!child.isShown()) {
- return false;
- }
- int action = event.getActionMasked();
- if (mState == STATE_DRAGGING && action == MotionEvent.ACTION_DOWN) {
- return true;
- }
- if (mViewDragHelper != null) {
- mViewDragHelper.processTouchEvent(event);
- }
- // Record the velocity
- if (action == MotionEvent.ACTION_DOWN) {
- reset();
- }
- if (mVelocityTracker == null) {
- mVelocityTracker = VelocityTracker.obtain();
- }
- mVelocityTracker.addMovement(event);
- // The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it
- // to capture the bottom sheet in case it is not captured and the touch slop is passed.
- if (action == MotionEvent.ACTION_MOVE && !mIgnoreEvents) {
- if (Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop()) {
- mViewDragHelper.captureChildView(child, event.getPointerId(event.getActionIndex()));
- }
- }
- return !mIgnoreEvents;
- }
-
- @Override
- public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child,
- View directTargetChild, View target, int nestedScrollAxes) {
- mLastNestedScrollDy = 0;
- mNestedScrolled = false;
- return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
- }
-
- @Override
- public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx,
- int dy, int[] consumed) {
- View scrollingChild = mNestedScrollingChildRef.get();
- if (target != scrollingChild) {
- return;
- }
- int currentTop = child.getTop();
- int newTop = currentTop - dy;
- if (dy > 0) { // Upward
- if (newTop < mMinOffset) {
- consumed[1] = currentTop - mMinOffset;
- ViewCompat.offsetTopAndBottom(child, -consumed[1]);
- setStateInternal(STATE_EXPANDED);
- } else {
- consumed[1] = dy;
- ViewCompat.offsetTopAndBottom(child, -dy);
- setStateInternal(STATE_DRAGGING);
- }
- } else if (dy < 0) { // Downward
- if (!target.canScrollVertically(-1)) {
- if (newTop <= mMaxOffset || mHideable) {
- consumed[1] = dy;
- ViewCompat.offsetTopAndBottom(child, -dy);
- setStateInternal(STATE_DRAGGING);
- } else {
- consumed[1] = currentTop - mMaxOffset;
- ViewCompat.offsetTopAndBottom(child, -consumed[1]);
- setStateInternal(STATE_COLLAPSED);
- }
- }
- }
- dispatchOnSlide(child.getTop());
- mLastNestedScrollDy = dy;
- mNestedScrolled = true;
- }
-
- @Override
- public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
- if (child.getTop() == mMinOffset) {
- setStateInternal(STATE_EXPANDED);
- return;
- }
- if (mNestedScrollingChildRef == null || target != mNestedScrollingChildRef.get()
- || !mNestedScrolled) {
- return;
- }
- int top;
- int targetState;
- if (mLastNestedScrollDy > 0) {
- top = mMinOffset;
- targetState = STATE_EXPANDED;
- } else if (mHideable && shouldHide(child, getYVelocity())) {
- top = mParentHeight;
- targetState = STATE_HIDDEN;
- } else if (mLastNestedScrollDy == 0) {
- int currentTop = child.getTop();
- if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) {
- top = mMinOffset;
- targetState = STATE_EXPANDED;
- } else {
- top = mMaxOffset;
- targetState = STATE_COLLAPSED;
- }
- } else {
- top = mMaxOffset;
- targetState = STATE_COLLAPSED;
- }
- if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
- setStateInternal(STATE_SETTLING);
- ViewCompat.postOnAnimation(child, new SettleRunnable(child, targetState));
- } else {
- setStateInternal(targetState);
- }
- mNestedScrolled = false;
- }
-
- @Override
- public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
- float velocityX, float velocityY) {
- return target == mNestedScrollingChildRef.get() &&
- (mState != STATE_EXPANDED ||
- super.onNestedPreFling(coordinatorLayout, child, target,
- velocityX, velocityY));
- }
-
- /**
- * Sets the height of the bottom sheet when it is collapsed.
- *
- * @param peekHeight The height of the collapsed bottom sheet in pixels, or
- * {@link #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically
- * at 16:9 ratio keyline.
- * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight
- */
- public final void setPeekHeight(int peekHeight) {
- boolean layout = false;
- if (peekHeight == PEEK_HEIGHT_AUTO) {
- if (!mPeekHeightAuto) {
- mPeekHeightAuto = true;
- layout = true;
- }
- } else if (mPeekHeightAuto || mPeekHeight != peekHeight) {
- mPeekHeightAuto = false;
- mPeekHeight = Math.max(0, peekHeight);
- mMaxOffset = mParentHeight - peekHeight;
- layout = true;
- }
- if (layout && mState == STATE_COLLAPSED && mViewRef != null) {
- V view = mViewRef.get();
- if (view != null) {
- view.requestLayout();
- }
- }
- }
-
- /**
- * Gets the height of the bottom sheet when it is collapsed.
- *
- * @return The height of the collapsed bottom sheet in pixels, or {@link #PEEK_HEIGHT_AUTO}
- * if the sheet is configured to peek automatically at 16:9 ratio keyline
- * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight
- */
- public final int getPeekHeight() {
- return mPeekHeightAuto ? PEEK_HEIGHT_AUTO : mPeekHeight;
- }
-
- /**
- * Sets whether this bottom sheet can hide when it is swiped down.
- *
- * @param hideable {@code true} to make this bottom sheet hideable.
- * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_hideable
- */
- public void setHideable(boolean hideable) {
- mHideable = hideable;
- }
-
- /**
- * Gets whether this bottom sheet can hide when it is swiped down.
- *
- * @return {@code true} if this bottom sheet can hide.
- * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_hideable
- */
- public boolean isHideable() {
- return mHideable;
- }
-
- /**
- * Sets whether this bottom sheet should skip the collapsed state when it is being hidden
- * after it is expanded once. Setting this to true has no effect unless the sheet is hideable.
- *
- * @param skipCollapsed True if the bottom sheet should skip the collapsed state.
- * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed
- */
- public void setSkipCollapsed(boolean skipCollapsed) {
- mSkipCollapsed = skipCollapsed;
- }
-
- /**
- * Sets whether this bottom sheet should skip the collapsed state when it is being hidden
- * after it is expanded once.
- *
- * @return Whether the bottom sheet should skip the collapsed state.
- * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed
- */
- public boolean getSkipCollapsed() {
- return mSkipCollapsed;
- }
-
- /**
- * Sets a callback to be notified of bottom sheet events.
- *
- * @param callback The callback to notify when bottom sheet events occur.
- */
- public void setBottomSheetCallback(BottomSheetCallback callback) {
- mCallback = callback;
- }
-
- /**
- * Sets the state of the bottom sheet. The bottom sheet will transition to that state with
- * animation.
- *
- * @param state One of {@link #STATE_COLLAPSED}, {@link #STATE_EXPANDED}, or
- * {@link #STATE_HIDDEN}.
- */
- public final void setState(final @State int state) {
- if (state == mState) {
- return;
- }
- if (mViewRef == null) {
- // The view is not laid out yet; modify mState and let onLayoutChild handle it later
- if (state == STATE_COLLAPSED || state == STATE_EXPANDED ||
- (mHideable && state == STATE_HIDDEN)) {
- mState = state;
- }
- return;
- }
- final V child = mViewRef.get();
- if (child == null) {
- return;
- }
- // Start the animation; wait until a pending layout if there is one.
- ViewParent parent = child.getParent();
- if (parent != null && parent.isLayoutRequested() && ViewCompat.isAttachedToWindow(child)) {
- child.post(new Runnable() {
- @Override
- public void run() {
- startSettlingAnimation(child, state);
- }
- });
- } else {
- startSettlingAnimation(child, state);
- }
- }
-
- /**
- * Gets the current state of the bottom sheet.
- *
- * @return One of {@link #STATE_EXPANDED}, {@link #STATE_COLLAPSED}, {@link #STATE_DRAGGING},
- * {@link #STATE_SETTLING}, and {@link #STATE_HIDDEN}.
- */
- @State
- public final int getState() {
- return mState;
- }
-
- void setStateInternal(@State int state) {
- if (mState == state) {
- return;
- }
- mState = state;
- View bottomSheet = mViewRef.get();
- if (bottomSheet != null && mCallback != null) {
- mCallback.onStateChanged(bottomSheet, state);
- }
- }
-
- private void reset() {
- mActivePointerId = ViewDragHelper.INVALID_POINTER;
- if (mVelocityTracker != null) {
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
- }
-
- boolean shouldHide(View child, float yvel) {
- if (mSkipCollapsed) {
- return true;
- }
- if (child.getTop() < mMaxOffset) {
- // It should not hide, but collapse.
- return false;
- }
- final float newTop = child.getTop() + yvel * HIDE_FRICTION;
- return Math.abs(newTop - mMaxOffset) / (float) mPeekHeight > HIDE_THRESHOLD;
- }
-
- @VisibleForTesting
- View findScrollingChild(View view) {
- if (ViewCompat.isNestedScrollingEnabled(view)) {
- return view;
- }
- if (view instanceof ViewGroup) {
- ViewGroup group = (ViewGroup) view;
- for (int i = 0, count = group.getChildCount(); i < count; i++) {
- View scrollingChild = findScrollingChild(group.getChildAt(i));
- if (scrollingChild != null) {
- return scrollingChild;
- }
- }
- }
- return null;
- }
-
- private float getYVelocity() {
- mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
- return mVelocityTracker.getYVelocity(mActivePointerId);
- }
-
- void startSettlingAnimation(View child, int state) {
- int top;
- if (state == STATE_COLLAPSED) {
- top = mMaxOffset;
- } else if (state == STATE_EXPANDED) {
- top = mMinOffset;
- } else if (mHideable && state == STATE_HIDDEN) {
- top = mParentHeight;
- } else {
- throw new IllegalArgumentException("Illegal state argument: " + state);
- }
- if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
- setStateInternal(STATE_SETTLING);
- ViewCompat.postOnAnimation(child, new SettleRunnable(child, state));
- } else {
- setStateInternal(state);
- }
- }
-
- private final ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
-
- @Override
- public boolean tryCaptureView(View child, int pointerId) {
- if (mState == STATE_DRAGGING) {
- return false;
- }
- if (mTouchingScrollingChild) {
- return false;
- }
- if (mState == STATE_EXPANDED && mActivePointerId == pointerId) {
- View scroll = mNestedScrollingChildRef.get();
- if (scroll != null && scroll.canScrollVertically(-1)) {
- // Let the content scroll up
- return false;
- }
- }
- return mViewRef != null && mViewRef.get() == child;
- }
-
- @Override
- public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
- dispatchOnSlide(top);
- }
-
- @Override
- public void onViewDragStateChanged(int state) {
- if (state == ViewDragHelper.STATE_DRAGGING) {
- setStateInternal(STATE_DRAGGING);
- }
- }
-
- @Override
- public void onViewReleased(View releasedChild, float xvel, float yvel) {
- int top;
- @State int targetState;
- if (yvel < 0) { // Moving up
- top = mMinOffset;
- targetState = STATE_EXPANDED;
- } else if (mHideable && shouldHide(releasedChild, yvel)) {
- top = mParentHeight;
- targetState = STATE_HIDDEN;
- } else if (yvel == 0.f) {
- int currentTop = releasedChild.getTop();
- if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) {
- top = mMinOffset;
- targetState = STATE_EXPANDED;
- } else {
- top = mMaxOffset;
- targetState = STATE_COLLAPSED;
- }
- } else {
- top = mMaxOffset;
- targetState = STATE_COLLAPSED;
- }
- if (mViewDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top)) {
- setStateInternal(STATE_SETTLING);
- ViewCompat.postOnAnimation(releasedChild,
- new SettleRunnable(releasedChild, targetState));
- } else {
- setStateInternal(targetState);
- }
- }
-
- @Override
- public int clampViewPositionVertical(View child, int top, int dy) {
- return MathUtils.clamp(top, mMinOffset, mHideable ? mParentHeight : mMaxOffset);
- }
-
- @Override
- public int clampViewPositionHorizontal(View child, int left, int dx) {
- return child.getLeft();
- }
-
- @Override
- public int getViewVerticalDragRange(View child) {
- if (mHideable) {
- return mParentHeight - mMinOffset;
- } else {
- return mMaxOffset - mMinOffset;
- }
- }
- };
-
- void dispatchOnSlide(int top) {
- View bottomSheet = mViewRef.get();
- if (bottomSheet != null && mCallback != null) {
- if (top > mMaxOffset) {
- mCallback.onSlide(bottomSheet, (float) (mMaxOffset - top) /
- (mParentHeight - mMaxOffset));
- } else {
- mCallback.onSlide(bottomSheet,
- (float) (mMaxOffset - top) / ((mMaxOffset - mMinOffset)));
- }
- }
- }
-
- @VisibleForTesting
- int getPeekHeightMin() {
- return mPeekHeightMin;
- }
-
- private class SettleRunnable implements Runnable {
-
- private final View mView;
-
- @State
- private final int mTargetState;
-
- SettleRunnable(View view, @State int targetState) {
- mView = view;
- mTargetState = targetState;
- }
-
- @Override
- public void run() {
- if (mViewDragHelper != null && mViewDragHelper.continueSettling(true)) {
- ViewCompat.postOnAnimation(mView, this);
- } else {
- setStateInternal(mTargetState);
- }
- }
- }
-
- protected static class SavedState extends AbsSavedState {
- @State
- final int state;
-
- public SavedState(Parcel source) {
- this(source, null);
- }
-
- public SavedState(Parcel source, ClassLoader loader) {
- super(source, loader);
- //noinspection ResourceType
- state = source.readInt();
- }
-
- public SavedState(Parcelable superState, @State int state) {
- super(superState);
- this.state = state;
- }
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- super.writeToParcel(out, flags);
- out.writeInt(state);
- }
-
- public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in, ClassLoader loader) {
- return new SavedState(in, loader);
- }
-
- @Override
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in, null);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
-
- /**
- * A utility function to get the {@link BottomSheetBehavior} associated with the {@code view}.
- *
- * @param view The {@link View} with {@link BottomSheetBehavior}.
- * @return The {@link BottomSheetBehavior} associated with the {@code view}.
- */
- @SuppressWarnings("unchecked")
- public static <V extends View> BottomSheetBehavior<V> from(V view) {
- ViewGroup.LayoutParams params = view.getLayoutParams();
- if (!(params instanceof CoordinatorLayout.LayoutParams)) {
- throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
- }
- CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params)
- .getBehavior();
- if (!(behavior instanceof BottomSheetBehavior)) {
- throw new IllegalArgumentException(
- "The view is not associated with BottomSheetBehavior");
- }
- return (BottomSheetBehavior<V>) behavior;
- }
-
-}
diff --git a/android/support/design/widget/BottomSheetDialog.java b/android/support/design/widget/BottomSheetDialog.java
deleted file mode 100644
index 19b5782..0000000
--- a/android/support/design/widget/BottomSheetDialog.java
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.os.Build;
-import android.os.Bundle;
-import android.support.annotation.LayoutRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.StyleRes;
-import android.support.design.R;
-import android.support.v4.view.AccessibilityDelegateCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
-import android.support.v7.app.AppCompatDialog;
-import android.util.TypedValue;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.Window;
-import android.view.WindowManager;
-import android.widget.FrameLayout;
-
-/**
- * Base class for {@link android.app.Dialog}s styled as a bottom sheet.
- */
-public class BottomSheetDialog extends AppCompatDialog {
-
- private BottomSheetBehavior<FrameLayout> mBehavior;
-
- boolean mCancelable = true;
- private boolean mCanceledOnTouchOutside = true;
- private boolean mCanceledOnTouchOutsideSet;
-
- public BottomSheetDialog(@NonNull Context context) {
- this(context, 0);
- }
-
- public BottomSheetDialog(@NonNull Context context, @StyleRes int theme) {
- super(context, getThemeResId(context, theme));
- // We hide the title bar for any style configuration. Otherwise, there will be a gap
- // above the bottom sheet when it is expanded.
- supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
- }
-
- protected BottomSheetDialog(@NonNull Context context, boolean cancelable,
- OnCancelListener cancelListener) {
- super(context, cancelable, cancelListener);
- supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
- mCancelable = cancelable;
- }
-
- @Override
- public void setContentView(@LayoutRes int layoutResId) {
- super.setContentView(wrapInBottomSheet(layoutResId, null, null));
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Window window = getWindow();
- if (window != null) {
- if (Build.VERSION.SDK_INT >= 21) {
- window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
- window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
- }
- window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT);
- }
- }
-
- @Override
- public void setContentView(View view) {
- super.setContentView(wrapInBottomSheet(0, view, null));
- }
-
- @Override
- public void setContentView(View view, ViewGroup.LayoutParams params) {
- super.setContentView(wrapInBottomSheet(0, view, params));
- }
-
- @Override
- public void setCancelable(boolean cancelable) {
- super.setCancelable(cancelable);
- if (mCancelable != cancelable) {
- mCancelable = cancelable;
- if (mBehavior != null) {
- mBehavior.setHideable(cancelable);
- }
- }
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- if (mBehavior != null) {
- mBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
- }
- }
-
- @Override
- public void setCanceledOnTouchOutside(boolean cancel) {
- super.setCanceledOnTouchOutside(cancel);
- if (cancel && !mCancelable) {
- mCancelable = true;
- }
- mCanceledOnTouchOutside = cancel;
- mCanceledOnTouchOutsideSet = true;
- }
-
- private View wrapInBottomSheet(int layoutResId, View view, ViewGroup.LayoutParams params) {
- final FrameLayout container = (FrameLayout) View.inflate(getContext(),
- R.layout.design_bottom_sheet_dialog, null);
- final CoordinatorLayout coordinator =
- (CoordinatorLayout) container.findViewById(R.id.coordinator);
- if (layoutResId != 0 && view == null) {
- view = getLayoutInflater().inflate(layoutResId, coordinator, false);
- }
- FrameLayout bottomSheet = (FrameLayout) coordinator.findViewById(R.id.design_bottom_sheet);
- mBehavior = BottomSheetBehavior.from(bottomSheet);
- mBehavior.setBottomSheetCallback(mBottomSheetCallback);
- mBehavior.setHideable(mCancelable);
- if (params == null) {
- bottomSheet.addView(view);
- } else {
- bottomSheet.addView(view, params);
- }
- // We treat the CoordinatorLayout as outside the dialog though it is technically inside
- coordinator.findViewById(R.id.touch_outside).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- if (mCancelable && isShowing() && shouldWindowCloseOnTouchOutside()) {
- cancel();
- }
- }
- });
- // Handle accessibility events
- ViewCompat.setAccessibilityDelegate(bottomSheet, new AccessibilityDelegateCompat() {
- @Override
- public void onInitializeAccessibilityNodeInfo(View host,
- AccessibilityNodeInfoCompat info) {
- super.onInitializeAccessibilityNodeInfo(host, info);
- if (mCancelable) {
- info.addAction(AccessibilityNodeInfoCompat.ACTION_DISMISS);
- info.setDismissable(true);
- } else {
- info.setDismissable(false);
- }
- }
-
- @Override
- public boolean performAccessibilityAction(View host, int action, Bundle args) {
- if (action == AccessibilityNodeInfoCompat.ACTION_DISMISS && mCancelable) {
- cancel();
- return true;
- }
- return super.performAccessibilityAction(host, action, args);
- }
- });
- bottomSheet.setOnTouchListener(new View.OnTouchListener() {
- @Override
- public boolean onTouch(View view, MotionEvent event) {
- // Consume the event and prevent it from falling through
- return true;
- }
- });
- return container;
- }
-
- boolean shouldWindowCloseOnTouchOutside() {
- if (!mCanceledOnTouchOutsideSet) {
- if (Build.VERSION.SDK_INT < 11) {
- mCanceledOnTouchOutside = true;
- } else {
- TypedArray a = getContext().obtainStyledAttributes(
- new int[]{android.R.attr.windowCloseOnTouchOutside});
- mCanceledOnTouchOutside = a.getBoolean(0, true);
- a.recycle();
- }
- mCanceledOnTouchOutsideSet = true;
- }
- return mCanceledOnTouchOutside;
- }
-
- private static int getThemeResId(Context context, int themeId) {
- if (themeId == 0) {
- // If the provided theme is 0, then retrieve the dialogTheme from our theme
- TypedValue outValue = new TypedValue();
- if (context.getTheme().resolveAttribute(
- R.attr.bottomSheetDialogTheme, outValue, true)) {
- themeId = outValue.resourceId;
- } else {
- // bottomSheetDialogTheme is not provided; we default to our light theme
- themeId = R.style.Theme_Design_Light_BottomSheetDialog;
- }
- }
- return themeId;
- }
-
- private BottomSheetBehavior.BottomSheetCallback mBottomSheetCallback
- = new BottomSheetBehavior.BottomSheetCallback() {
- @Override
- public void onStateChanged(@NonNull View bottomSheet,
- @BottomSheetBehavior.State int newState) {
- if (newState == BottomSheetBehavior.STATE_HIDDEN) {
- cancel();
- }
- }
-
- @Override
- public void onSlide(@NonNull View bottomSheet, float slideOffset) {
- }
- };
-
-}
diff --git a/android/support/design/widget/BottomSheetDialogFragment.java b/android/support/design/widget/BottomSheetDialogFragment.java
deleted file mode 100644
index 8842988..0000000
--- a/android/support/design/widget/BottomSheetDialogFragment.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.app.Dialog;
-import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
-import android.support.v7.app.AppCompatDialogFragment;
-
-/**
- * Modal bottom sheet. This is a version of {@link DialogFragment} that shows a bottom sheet
- * using {@link BottomSheetDialog} instead of a floating dialog.
- */
-public class BottomSheetDialogFragment extends AppCompatDialogFragment {
-
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- return new BottomSheetDialog(getContext(), getTheme());
- }
-
-}
diff --git a/android/support/design/widget/CheckableImageButton.java b/android/support/design/widget/CheckableImageButton.java
deleted file mode 100644
index f274581..0000000
--- a/android/support/design/widget/CheckableImageButton.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * 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.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.support.annotation.RestrictTo;
-import android.support.v4.view.AccessibilityDelegateCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.accessibility.AccessibilityEventCompat;
-import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
-import android.support.v7.widget.AppCompatImageButton;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.accessibility.AccessibilityEvent;
-import android.widget.Checkable;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class CheckableImageButton extends AppCompatImageButton implements Checkable {
-
- private static final int[] DRAWABLE_STATE_CHECKED = new int[]{android.R.attr.state_checked};
-
- private boolean mChecked;
-
- public CheckableImageButton(Context context) {
- this(context, null);
- }
-
- public CheckableImageButton(Context context, AttributeSet attrs) {
- this(context, attrs, android.support.v7.appcompat.R.attr.imageButtonStyle);
- }
-
- public CheckableImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegateCompat() {
- @Override
- public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
- super.onInitializeAccessibilityEvent(host, event);
- event.setChecked(isChecked());
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfo(View host,
- AccessibilityNodeInfoCompat info) {
- super.onInitializeAccessibilityNodeInfo(host, info);
- info.setCheckable(true);
- info.setChecked(isChecked());
- }
- });
- }
-
- @Override
- public void setChecked(boolean checked) {
- if (mChecked != checked) {
- mChecked = checked;
- refreshDrawableState();
- sendAccessibilityEvent(
- AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED);
- }
- }
-
- @Override
- public boolean isChecked() {
- return mChecked;
- }
-
- @Override
- public void toggle() {
- setChecked(!mChecked);
- }
-
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- if (mChecked) {
- return mergeDrawableStates(
- super.onCreateDrawableState(extraSpace + DRAWABLE_STATE_CHECKED.length),
- DRAWABLE_STATE_CHECKED);
- } else {
- return super.onCreateDrawableState(extraSpace);
- }
- }
-}
diff --git a/android/support/design/widget/CircularBorderDrawable.java b/android/support/design/widget/CircularBorderDrawable.java
deleted file mode 100644
index 617a501..0000000
--- a/android/support/design/widget/CircularBorderDrawable.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.content.res.ColorStateList;
-import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.LinearGradient;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.Shader;
-import android.graphics.drawable.Drawable;
-import android.support.v4.graphics.ColorUtils;
-
-/**
- * A drawable which draws an oval 'border'.
- */
-class CircularBorderDrawable extends Drawable {
-
- /**
- * We actually draw the stroke wider than the border size given. This is to reduce any
- * potential transparent space caused by anti-aliasing and padding rounding.
- * This value defines the multiplier used to determine to draw stroke width.
- */
- private static final float DRAW_STROKE_WIDTH_MULTIPLE = 1.3333f;
-
- final Paint mPaint;
- final Rect mRect = new Rect();
- final RectF mRectF = new RectF();
-
- float mBorderWidth;
-
- private int mTopOuterStrokeColor;
- private int mTopInnerStrokeColor;
- private int mBottomOuterStrokeColor;
- private int mBottomInnerStrokeColor;
-
- private ColorStateList mBorderTint;
- private int mCurrentBorderTintColor;
-
- private boolean mInvalidateShader = true;
-
- private float mRotation;
-
- public CircularBorderDrawable() {
- mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- mPaint.setStyle(Paint.Style.STROKE);
- }
-
- void setGradientColors(int topOuterStrokeColor, int topInnerStrokeColor,
- int bottomOuterStrokeColor, int bottomInnerStrokeColor) {
- mTopOuterStrokeColor = topOuterStrokeColor;
- mTopInnerStrokeColor = topInnerStrokeColor;
- mBottomOuterStrokeColor = bottomOuterStrokeColor;
- mBottomInnerStrokeColor = bottomInnerStrokeColor;
- }
-
- /**
- * Set the border width
- */
- void setBorderWidth(float width) {
- if (mBorderWidth != width) {
- mBorderWidth = width;
- mPaint.setStrokeWidth(width * DRAW_STROKE_WIDTH_MULTIPLE);
- mInvalidateShader = true;
- invalidateSelf();
- }
- }
-
- @Override
- public void draw(Canvas canvas) {
- if (mInvalidateShader) {
- mPaint.setShader(createGradientShader());
- mInvalidateShader = false;
- }
-
- final float halfBorderWidth = mPaint.getStrokeWidth() / 2f;
- final RectF rectF = mRectF;
-
- // We need to inset the oval bounds by half the border width. This is because stroke draws
- // the center of the border on the dimension. Whereas we want the stroke on the inside.
- copyBounds(mRect);
- rectF.set(mRect);
- rectF.left += halfBorderWidth;
- rectF.top += halfBorderWidth;
- rectF.right -= halfBorderWidth;
- rectF.bottom -= halfBorderWidth;
-
- canvas.save();
- canvas.rotate(mRotation, rectF.centerX(), rectF.centerY());
- // Draw the oval
- canvas.drawOval(rectF, mPaint);
- canvas.restore();
- }
-
- @Override
- public boolean getPadding(Rect padding) {
- final int borderWidth = Math.round(mBorderWidth);
- padding.set(borderWidth, borderWidth, borderWidth, borderWidth);
- return true;
- }
-
- @Override
- public void setAlpha(int alpha) {
- mPaint.setAlpha(alpha);
- invalidateSelf();
- }
-
- void setBorderTint(ColorStateList tint) {
- if (tint != null) {
- mCurrentBorderTintColor = tint.getColorForState(getState(), mCurrentBorderTintColor);
- }
- mBorderTint = tint;
- mInvalidateShader = true;
- invalidateSelf();
- }
-
- @Override
- public void setColorFilter(ColorFilter colorFilter) {
- mPaint.setColorFilter(colorFilter);
- invalidateSelf();
- }
-
- @Override
- public int getOpacity() {
- return mBorderWidth > 0 ? PixelFormat.TRANSLUCENT : PixelFormat.TRANSPARENT;
- }
-
- final void setRotation(float rotation) {
- if (rotation != mRotation) {
- mRotation = rotation;
- invalidateSelf();
- }
- }
-
- @Override
- protected void onBoundsChange(Rect bounds) {
- mInvalidateShader = true;
- }
-
- @Override
- public boolean isStateful() {
- return (mBorderTint != null && mBorderTint.isStateful()) || super.isStateful();
- }
-
- @Override
- protected boolean onStateChange(int[] state) {
- if (mBorderTint != null) {
- final int newColor = mBorderTint.getColorForState(state, mCurrentBorderTintColor);
- if (newColor != mCurrentBorderTintColor) {
- mInvalidateShader = true;
- mCurrentBorderTintColor = newColor;
- }
- }
- if (mInvalidateShader) {
- invalidateSelf();
- }
- return mInvalidateShader;
- }
-
- /**
- * Creates a vertical {@link LinearGradient}
- * @return
- */
- private Shader createGradientShader() {
- final Rect rect = mRect;
- copyBounds(rect);
-
- final float borderRatio = mBorderWidth / rect.height();
-
- final int[] colors = new int[6];
- colors[0] = ColorUtils.compositeColors(mTopOuterStrokeColor, mCurrentBorderTintColor);
- colors[1] = ColorUtils.compositeColors(mTopInnerStrokeColor, mCurrentBorderTintColor);
- colors[2] = ColorUtils.compositeColors(
- ColorUtils.setAlphaComponent(mTopInnerStrokeColor, 0), mCurrentBorderTintColor);
- colors[3] = ColorUtils.compositeColors(
- ColorUtils.setAlphaComponent(mBottomInnerStrokeColor, 0), mCurrentBorderTintColor);
- colors[4] = ColorUtils.compositeColors(mBottomInnerStrokeColor, mCurrentBorderTintColor);
- colors[5] = ColorUtils.compositeColors(mBottomOuterStrokeColor, mCurrentBorderTintColor);
-
- final float[] positions = new float[6];
- positions[0] = 0f;
- positions[1] = borderRatio;
- positions[2] = 0.5f;
- positions[3] = 0.5f;
- positions[4] = 1f - borderRatio;
- positions[5] = 1f;
-
- return new LinearGradient(
- 0, rect.top,
- 0, rect.bottom,
- colors, positions,
- Shader.TileMode.CLAMP);
- }
-}
diff --git a/android/support/design/widget/CircularBorderDrawableLollipop.java b/android/support/design/widget/CircularBorderDrawableLollipop.java
deleted file mode 100644
index 8008404..0000000
--- a/android/support/design/widget/CircularBorderDrawableLollipop.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.graphics.Outline;
-import android.support.annotation.RequiresApi;
-
-/**
- * Lollipop version of {@link CircularBorderDrawable}.
- */
-@RequiresApi(21)
-class CircularBorderDrawableLollipop extends CircularBorderDrawable {
-
- @Override
- public void getOutline(Outline outline) {
- copyBounds(mRect);
- outline.setOval(mRect);
- }
-
-}
diff --git a/android/support/design/widget/CollapsingTextHelper.java b/android/support/design/widget/CollapsingTextHelper.java
deleted file mode 100644
index a33cabc..0000000
--- a/android/support/design/widget/CollapsingTextHelper.java
+++ /dev/null
@@ -1,723 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.Typeface;
-import android.os.Build;
-import android.support.annotation.ColorInt;
-import android.support.v4.math.MathUtils;
-import android.support.v4.text.TextDirectionHeuristicsCompat;
-import android.support.v4.view.GravityCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v7.widget.TintTypedArray;
-import android.text.TextPaint;
-import android.text.TextUtils;
-import android.view.Gravity;
-import android.view.View;
-import android.view.animation.Interpolator;
-
-final class CollapsingTextHelper {
-
- // Pre-JB-MR2 doesn't support HW accelerated canvas scaled text so we will workaround it
- // by using our own texture
- private static final boolean USE_SCALING_TEXTURE = Build.VERSION.SDK_INT < 18;
-
- private static final boolean DEBUG_DRAW = false;
- private static final Paint DEBUG_DRAW_PAINT;
- static {
- DEBUG_DRAW_PAINT = DEBUG_DRAW ? new Paint() : null;
- if (DEBUG_DRAW_PAINT != null) {
- DEBUG_DRAW_PAINT.setAntiAlias(true);
- DEBUG_DRAW_PAINT.setColor(Color.MAGENTA);
- }
- }
-
- private final View mView;
-
- private boolean mDrawTitle;
- private float mExpandedFraction;
-
- private final Rect mExpandedBounds;
- private final Rect mCollapsedBounds;
- private final RectF mCurrentBounds;
- private int mExpandedTextGravity = Gravity.CENTER_VERTICAL;
- private int mCollapsedTextGravity = Gravity.CENTER_VERTICAL;
- private float mExpandedTextSize = 15;
- private float mCollapsedTextSize = 15;
- private ColorStateList mExpandedTextColor;
- private ColorStateList mCollapsedTextColor;
-
- private float mExpandedDrawY;
- private float mCollapsedDrawY;
- private float mExpandedDrawX;
- private float mCollapsedDrawX;
- private float mCurrentDrawX;
- private float mCurrentDrawY;
- private Typeface mCollapsedTypeface;
- private Typeface mExpandedTypeface;
- private Typeface mCurrentTypeface;
-
- private CharSequence mText;
- private CharSequence mTextToDraw;
- private boolean mIsRtl;
-
- private boolean mUseTexture;
- private Bitmap mExpandedTitleTexture;
- private Paint mTexturePaint;
- private float mTextureAscent;
- private float mTextureDescent;
-
- private float mScale;
- private float mCurrentTextSize;
-
- private int[] mState;
-
- private boolean mBoundsChanged;
-
- private final TextPaint mTextPaint;
-
- private Interpolator mPositionInterpolator;
- private Interpolator mTextSizeInterpolator;
-
- private float mCollapsedShadowRadius, mCollapsedShadowDx, mCollapsedShadowDy;
- private int mCollapsedShadowColor;
-
- private float mExpandedShadowRadius, mExpandedShadowDx, mExpandedShadowDy;
- private int mExpandedShadowColor;
-
- public CollapsingTextHelper(View view) {
- mView = view;
-
- mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG);
-
- mCollapsedBounds = new Rect();
- mExpandedBounds = new Rect();
- mCurrentBounds = new RectF();
- }
-
- void setTextSizeInterpolator(Interpolator interpolator) {
- mTextSizeInterpolator = interpolator;
- recalculate();
- }
-
- void setPositionInterpolator(Interpolator interpolator) {
- mPositionInterpolator = interpolator;
- recalculate();
- }
-
- void setExpandedTextSize(float textSize) {
- if (mExpandedTextSize != textSize) {
- mExpandedTextSize = textSize;
- recalculate();
- }
- }
-
- void setCollapsedTextSize(float textSize) {
- if (mCollapsedTextSize != textSize) {
- mCollapsedTextSize = textSize;
- recalculate();
- }
- }
-
- void setCollapsedTextColor(ColorStateList textColor) {
- if (mCollapsedTextColor != textColor) {
- mCollapsedTextColor = textColor;
- recalculate();
- }
- }
-
- void setExpandedTextColor(ColorStateList textColor) {
- if (mExpandedTextColor != textColor) {
- mExpandedTextColor = textColor;
- recalculate();
- }
- }
-
- void setExpandedBounds(int left, int top, int right, int bottom) {
- if (!rectEquals(mExpandedBounds, left, top, right, bottom)) {
- mExpandedBounds.set(left, top, right, bottom);
- mBoundsChanged = true;
- onBoundsChanged();
- }
- }
-
- void setCollapsedBounds(int left, int top, int right, int bottom) {
- if (!rectEquals(mCollapsedBounds, left, top, right, bottom)) {
- mCollapsedBounds.set(left, top, right, bottom);
- mBoundsChanged = true;
- onBoundsChanged();
- }
- }
-
- void onBoundsChanged() {
- mDrawTitle = mCollapsedBounds.width() > 0 && mCollapsedBounds.height() > 0
- && mExpandedBounds.width() > 0 && mExpandedBounds.height() > 0;
- }
-
- void setExpandedTextGravity(int gravity) {
- if (mExpandedTextGravity != gravity) {
- mExpandedTextGravity = gravity;
- recalculate();
- }
- }
-
- int getExpandedTextGravity() {
- return mExpandedTextGravity;
- }
-
- void setCollapsedTextGravity(int gravity) {
- if (mCollapsedTextGravity != gravity) {
- mCollapsedTextGravity = gravity;
- recalculate();
- }
- }
-
- int getCollapsedTextGravity() {
- return mCollapsedTextGravity;
- }
-
- void setCollapsedTextAppearance(int resId) {
- TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), resId,
- android.support.v7.appcompat.R.styleable.TextAppearance);
- if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor)) {
- mCollapsedTextColor = a.getColorStateList(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor);
- }
- if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize)) {
- mCollapsedTextSize = a.getDimensionPixelSize(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize,
- (int) mCollapsedTextSize);
- }
- mCollapsedShadowColor = a.getInt(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowColor, 0);
- mCollapsedShadowDx = a.getFloat(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDx, 0);
- mCollapsedShadowDy = a.getFloat(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDy, 0);
- mCollapsedShadowRadius = a.getFloat(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowRadius, 0);
- a.recycle();
-
- if (Build.VERSION.SDK_INT >= 16) {
- mCollapsedTypeface = readFontFamilyTypeface(resId);
- }
-
- recalculate();
- }
-
- void setExpandedTextAppearance(int resId) {
- TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), resId,
- android.support.v7.appcompat.R.styleable.TextAppearance);
- if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor)) {
- mExpandedTextColor = a.getColorStateList(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor);
- }
- if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize)) {
- mExpandedTextSize = a.getDimensionPixelSize(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize,
- (int) mExpandedTextSize);
- }
- mExpandedShadowColor = a.getInt(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowColor, 0);
- mExpandedShadowDx = a.getFloat(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDx, 0);
- mExpandedShadowDy = a.getFloat(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDy, 0);
- mExpandedShadowRadius = a.getFloat(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowRadius, 0);
- a.recycle();
-
- if (Build.VERSION.SDK_INT >= 16) {
- mExpandedTypeface = readFontFamilyTypeface(resId);
- }
-
- recalculate();
- }
-
- private Typeface readFontFamilyTypeface(int resId) {
- final TypedArray a = mView.getContext().obtainStyledAttributes(resId,
- new int[]{android.R.attr.fontFamily});
- try {
- final String family = a.getString(0);
- if (family != null) {
- return Typeface.create(family, Typeface.NORMAL);
- }
- } finally {
- a.recycle();
- }
- return null;
- }
-
- void setCollapsedTypeface(Typeface typeface) {
- if (areTypefacesDifferent(mCollapsedTypeface, typeface)) {
- mCollapsedTypeface = typeface;
- recalculate();
- }
- }
-
- void setExpandedTypeface(Typeface typeface) {
- if (areTypefacesDifferent(mExpandedTypeface, typeface)) {
- mExpandedTypeface = typeface;
- recalculate();
- }
- }
-
- void setTypefaces(Typeface typeface) {
- mCollapsedTypeface = mExpandedTypeface = typeface;
- recalculate();
- }
-
- Typeface getCollapsedTypeface() {
- return mCollapsedTypeface != null ? mCollapsedTypeface : Typeface.DEFAULT;
- }
-
- Typeface getExpandedTypeface() {
- return mExpandedTypeface != null ? mExpandedTypeface : Typeface.DEFAULT;
- }
-
- /**
- * Set the value indicating the current scroll value. This decides how much of the
- * background will be displayed, as well as the title metrics/positioning.
- *
- * A value of {@code 0.0} indicates that the layout is fully expanded.
- * A value of {@code 1.0} indicates that the layout is fully collapsed.
- */
- void setExpansionFraction(float fraction) {
- fraction = MathUtils.clamp(fraction, 0f, 1f);
-
- if (fraction != mExpandedFraction) {
- mExpandedFraction = fraction;
- calculateCurrentOffsets();
- }
- }
-
- final boolean setState(final int[] state) {
- mState = state;
-
- if (isStateful()) {
- recalculate();
- return true;
- }
-
- return false;
- }
-
- final boolean isStateful() {
- return (mCollapsedTextColor != null && mCollapsedTextColor.isStateful())
- || (mExpandedTextColor != null && mExpandedTextColor.isStateful());
- }
-
- float getExpansionFraction() {
- return mExpandedFraction;
- }
-
- float getCollapsedTextSize() {
- return mCollapsedTextSize;
- }
-
- float getExpandedTextSize() {
- return mExpandedTextSize;
- }
-
- private void calculateCurrentOffsets() {
- calculateOffsets(mExpandedFraction);
- }
-
- private void calculateOffsets(final float fraction) {
- interpolateBounds(fraction);
- mCurrentDrawX = lerp(mExpandedDrawX, mCollapsedDrawX, fraction,
- mPositionInterpolator);
- mCurrentDrawY = lerp(mExpandedDrawY, mCollapsedDrawY, fraction,
- mPositionInterpolator);
-
- setInterpolatedTextSize(lerp(mExpandedTextSize, mCollapsedTextSize,
- fraction, mTextSizeInterpolator));
-
- if (mCollapsedTextColor != mExpandedTextColor) {
- // If the collapsed and expanded text colors are different, blend them based on the
- // fraction
- mTextPaint.setColor(blendColors(
- getCurrentExpandedTextColor(), getCurrentCollapsedTextColor(), fraction));
- } else {
- mTextPaint.setColor(getCurrentCollapsedTextColor());
- }
-
- mTextPaint.setShadowLayer(
- lerp(mExpandedShadowRadius, mCollapsedShadowRadius, fraction, null),
- lerp(mExpandedShadowDx, mCollapsedShadowDx, fraction, null),
- lerp(mExpandedShadowDy, mCollapsedShadowDy, fraction, null),
- blendColors(mExpandedShadowColor, mCollapsedShadowColor, fraction));
-
- ViewCompat.postInvalidateOnAnimation(mView);
- }
-
- @ColorInt
- private int getCurrentExpandedTextColor() {
- if (mState != null) {
- return mExpandedTextColor.getColorForState(mState, 0);
- } else {
- return mExpandedTextColor.getDefaultColor();
- }
- }
-
- @ColorInt
- private int getCurrentCollapsedTextColor() {
- if (mState != null) {
- return mCollapsedTextColor.getColorForState(mState, 0);
- } else {
- return mCollapsedTextColor.getDefaultColor();
- }
- }
-
- private void calculateBaseOffsets() {
- final float currentTextSize = mCurrentTextSize;
-
- // We then calculate the collapsed text size, using the same logic
- calculateUsingTextSize(mCollapsedTextSize);
- float width = mTextToDraw != null ?
- mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()) : 0;
- final int collapsedAbsGravity = GravityCompat.getAbsoluteGravity(mCollapsedTextGravity,
- mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR);
- switch (collapsedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
- case Gravity.BOTTOM:
- mCollapsedDrawY = mCollapsedBounds.bottom;
- break;
- case Gravity.TOP:
- mCollapsedDrawY = mCollapsedBounds.top - mTextPaint.ascent();
- break;
- case Gravity.CENTER_VERTICAL:
- default:
- float textHeight = mTextPaint.descent() - mTextPaint.ascent();
- float textOffset = (textHeight / 2) - mTextPaint.descent();
- mCollapsedDrawY = mCollapsedBounds.centerY() + textOffset;
- break;
- }
- switch (collapsedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
- case Gravity.CENTER_HORIZONTAL:
- mCollapsedDrawX = mCollapsedBounds.centerX() - (width / 2);
- break;
- case Gravity.RIGHT:
- mCollapsedDrawX = mCollapsedBounds.right - width;
- break;
- case Gravity.LEFT:
- default:
- mCollapsedDrawX = mCollapsedBounds.left;
- break;
- }
-
- calculateUsingTextSize(mExpandedTextSize);
- width = mTextToDraw != null
- ? mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()) : 0;
- final int expandedAbsGravity = GravityCompat.getAbsoluteGravity(mExpandedTextGravity,
- mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR);
- switch (expandedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
- case Gravity.BOTTOM:
- mExpandedDrawY = mExpandedBounds.bottom;
- break;
- case Gravity.TOP:
- mExpandedDrawY = mExpandedBounds.top - mTextPaint.ascent();
- break;
- case Gravity.CENTER_VERTICAL:
- default:
- float textHeight = mTextPaint.descent() - mTextPaint.ascent();
- float textOffset = (textHeight / 2) - mTextPaint.descent();
- mExpandedDrawY = mExpandedBounds.centerY() + textOffset;
- break;
- }
- switch (expandedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
- case Gravity.CENTER_HORIZONTAL:
- mExpandedDrawX = mExpandedBounds.centerX() - (width / 2);
- break;
- case Gravity.RIGHT:
- mExpandedDrawX = mExpandedBounds.right - width;
- break;
- case Gravity.LEFT:
- default:
- mExpandedDrawX = mExpandedBounds.left;
- break;
- }
-
- // The bounds have changed so we need to clear the texture
- clearTexture();
- // Now reset the text size back to the original
- setInterpolatedTextSize(currentTextSize);
- }
-
- private void interpolateBounds(float fraction) {
- mCurrentBounds.left = lerp(mExpandedBounds.left, mCollapsedBounds.left,
- fraction, mPositionInterpolator);
- mCurrentBounds.top = lerp(mExpandedDrawY, mCollapsedDrawY,
- fraction, mPositionInterpolator);
- mCurrentBounds.right = lerp(mExpandedBounds.right, mCollapsedBounds.right,
- fraction, mPositionInterpolator);
- mCurrentBounds.bottom = lerp(mExpandedBounds.bottom, mCollapsedBounds.bottom,
- fraction, mPositionInterpolator);
- }
-
- public void draw(Canvas canvas) {
- final int saveCount = canvas.save();
-
- if (mTextToDraw != null && mDrawTitle) {
- float x = mCurrentDrawX;
- float y = mCurrentDrawY;
-
- final boolean drawTexture = mUseTexture && mExpandedTitleTexture != null;
-
- final float ascent;
- final float descent;
- if (drawTexture) {
- ascent = mTextureAscent * mScale;
- descent = mTextureDescent * mScale;
- } else {
- ascent = mTextPaint.ascent() * mScale;
- descent = mTextPaint.descent() * mScale;
- }
-
- if (DEBUG_DRAW) {
- // Just a debug tool, which drawn a magenta rect in the text bounds
- canvas.drawRect(mCurrentBounds.left, y + ascent, mCurrentBounds.right, y + descent,
- DEBUG_DRAW_PAINT);
- }
-
- if (drawTexture) {
- y += ascent;
- }
-
- if (mScale != 1f) {
- canvas.scale(mScale, mScale, x, y);
- }
-
- if (drawTexture) {
- // If we should use a texture, draw it instead of text
- canvas.drawBitmap(mExpandedTitleTexture, x, y, mTexturePaint);
- } else {
- canvas.drawText(mTextToDraw, 0, mTextToDraw.length(), x, y, mTextPaint);
- }
- }
-
- canvas.restoreToCount(saveCount);
- }
-
- private boolean calculateIsRtl(CharSequence text) {
- final boolean defaultIsRtl = ViewCompat.getLayoutDirection(mView)
- == ViewCompat.LAYOUT_DIRECTION_RTL;
- return (defaultIsRtl
- ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL
- : TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR).isRtl(text, 0, text.length());
- }
-
- private void setInterpolatedTextSize(float textSize) {
- calculateUsingTextSize(textSize);
-
- // Use our texture if the scale isn't 1.0
- mUseTexture = USE_SCALING_TEXTURE && mScale != 1f;
-
- if (mUseTexture) {
- // Make sure we have an expanded texture if needed
- ensureExpandedTexture();
- }
-
- ViewCompat.postInvalidateOnAnimation(mView);
- }
-
- private boolean areTypefacesDifferent(Typeface first, Typeface second) {
- return (first != null && !first.equals(second)) || (first == null && second != null);
- }
-
- private void calculateUsingTextSize(final float textSize) {
- if (mText == null) return;
-
- final float collapsedWidth = mCollapsedBounds.width();
- final float expandedWidth = mExpandedBounds.width();
-
- final float availableWidth;
- final float newTextSize;
- boolean updateDrawText = false;
-
- if (isClose(textSize, mCollapsedTextSize)) {
- newTextSize = mCollapsedTextSize;
- mScale = 1f;
- if (areTypefacesDifferent(mCurrentTypeface, mCollapsedTypeface)) {
- mCurrentTypeface = mCollapsedTypeface;
- updateDrawText = true;
- }
- availableWidth = collapsedWidth;
- } else {
- newTextSize = mExpandedTextSize;
- if (areTypefacesDifferent(mCurrentTypeface, mExpandedTypeface)) {
- mCurrentTypeface = mExpandedTypeface;
- updateDrawText = true;
- }
- if (isClose(textSize, mExpandedTextSize)) {
- // If we're close to the expanded text size, snap to it and use a scale of 1
- mScale = 1f;
- } else {
- // Else, we'll scale down from the expanded text size
- mScale = textSize / mExpandedTextSize;
- }
-
- final float textSizeRatio = mCollapsedTextSize / mExpandedTextSize;
- // This is the size of the expanded bounds when it is scaled to match the
- // collapsed text size
- final float scaledDownWidth = expandedWidth * textSizeRatio;
-
- if (scaledDownWidth > collapsedWidth) {
- // If the scaled down size is larger than the actual collapsed width, we need to
- // cap the available width so that when the expanded text scales down, it matches
- // the collapsed width
- availableWidth = Math.min(collapsedWidth / textSizeRatio, expandedWidth);
- } else {
- // Otherwise we'll just use the expanded width
- availableWidth = expandedWidth;
- }
- }
-
- if (availableWidth > 0) {
- updateDrawText = (mCurrentTextSize != newTextSize) || mBoundsChanged || updateDrawText;
- mCurrentTextSize = newTextSize;
- mBoundsChanged = false;
- }
-
- if (mTextToDraw == null || updateDrawText) {
- mTextPaint.setTextSize(mCurrentTextSize);
- mTextPaint.setTypeface(mCurrentTypeface);
- // Use linear text scaling if we're scaling the canvas
- mTextPaint.setLinearText(mScale != 1f);
-
- // If we don't currently have text to draw, or the text size has changed, ellipsize...
- final CharSequence title = TextUtils.ellipsize(mText, mTextPaint,
- availableWidth, TextUtils.TruncateAt.END);
- if (!TextUtils.equals(title, mTextToDraw)) {
- mTextToDraw = title;
- mIsRtl = calculateIsRtl(mTextToDraw);
- }
- }
- }
-
- private void ensureExpandedTexture() {
- if (mExpandedTitleTexture != null || mExpandedBounds.isEmpty()
- || TextUtils.isEmpty(mTextToDraw)) {
- return;
- }
-
- calculateOffsets(0f);
- mTextureAscent = mTextPaint.ascent();
- mTextureDescent = mTextPaint.descent();
-
- final int w = Math.round(mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()));
- final int h = Math.round(mTextureDescent - mTextureAscent);
-
- if (w <= 0 || h <= 0) {
- return; // If the width or height are 0, return
- }
-
- mExpandedTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
-
- Canvas c = new Canvas(mExpandedTitleTexture);
- c.drawText(mTextToDraw, 0, mTextToDraw.length(), 0, h - mTextPaint.descent(), mTextPaint);
-
- if (mTexturePaint == null) {
- // Make sure we have a paint
- mTexturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
- }
- }
-
- public void recalculate() {
- if (mView.getHeight() > 0 && mView.getWidth() > 0) {
- // If we've already been laid out, calculate everything now otherwise we'll wait
- // until a layout
- calculateBaseOffsets();
- calculateCurrentOffsets();
- }
- }
-
- /**
- * Set the title to display
- *
- * @param text
- */
- void setText(CharSequence text) {
- if (text == null || !text.equals(mText)) {
- mText = text;
- mTextToDraw = null;
- clearTexture();
- recalculate();
- }
- }
-
- CharSequence getText() {
- return mText;
- }
-
- private void clearTexture() {
- if (mExpandedTitleTexture != null) {
- mExpandedTitleTexture.recycle();
- mExpandedTitleTexture = null;
- }
- }
-
- /**
- * Returns true if {@code value} is 'close' to it's closest decimal value. Close is currently
- * defined as it's difference being < 0.001.
- */
- private static boolean isClose(float value, float targetValue) {
- return Math.abs(value - targetValue) < 0.001f;
- }
-
- ColorStateList getExpandedTextColor() {
- return mExpandedTextColor;
- }
-
- ColorStateList getCollapsedTextColor() {
- return mCollapsedTextColor;
- }
-
- /**
- * Blend {@code color1} and {@code color2} using the given ratio.
- *
- * @param ratio of which to blend. 0.0 will return {@code color1}, 0.5 will give an even blend,
- * 1.0 will return {@code color2}.
- */
- private static int blendColors(int color1, int color2, float ratio) {
- final float inverseRatio = 1f - ratio;
- float a = (Color.alpha(color1) * inverseRatio) + (Color.alpha(color2) * ratio);
- float r = (Color.red(color1) * inverseRatio) + (Color.red(color2) * ratio);
- float g = (Color.green(color1) * inverseRatio) + (Color.green(color2) * ratio);
- float b = (Color.blue(color1) * inverseRatio) + (Color.blue(color2) * ratio);
- return Color.argb((int) a, (int) r, (int) g, (int) b);
- }
-
- private static float lerp(float startValue, float endValue, float fraction,
- Interpolator interpolator) {
- if (interpolator != null) {
- fraction = interpolator.getInterpolation(fraction);
- }
- return AnimationUtils.lerp(startValue, endValue, fraction);
- }
-
- private static boolean rectEquals(Rect r, int left, int top, int right, int bottom) {
- return !(r.left != left || r.top != top || r.right != right || r.bottom != bottom);
- }
-}
diff --git a/android/support/design/widget/CollapsingToolbarLayout.java b/android/support/design/widget/CollapsingToolbarLayout.java
deleted file mode 100644
index 8c9b7d4..0000000
--- a/android/support/design/widget/CollapsingToolbarLayout.java
+++ /dev/null
@@ -1,1308 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.Typeface;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.ColorInt;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.IntDef;
-import android.support.annotation.IntRange;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RequiresApi;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.StyleRes;
-import android.support.design.R;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import android.support.v4.math.MathUtils;
-import android.support.v4.util.ObjectsCompat;
-import android.support.v4.view.GravityCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.WindowInsetsCompat;
-import android.support.v4.widget.ViewGroupUtils;
-import android.support.v7.widget.Toolbar;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.widget.FrameLayout;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * CollapsingToolbarLayout is a wrapper for {@link Toolbar} which implements a collapsing app bar.
- * It is designed to be used as a direct child of a {@link AppBarLayout}.
- * CollapsingToolbarLayout contains the following features:
- *
- * <h4>Collapsing title</h4>
- * A title which is larger when the layout is fully visible but collapses and becomes smaller as
- * the layout is scrolled off screen. You can set the title to display via
- * {@link #setTitle(CharSequence)}. The title appearance can be tweaked via the
- * {@code collapsedTextAppearance} and {@code expandedTextAppearance} attributes.
- *
- * <h4>Content scrim</h4>
- * A full-bleed scrim which is show or hidden when the scroll position has hit a certain threshold.
- * You can change this via {@link #setContentScrim(Drawable)}.
- *
- * <h4>Status bar scrim</h4>
- * A scrim which is show or hidden behind the status bar when the scroll position has hit a certain
- * threshold. You can change this via {@link #setStatusBarScrim(Drawable)}. This only works
- * on {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} devices when we set to fit system
- * windows.
- *
- * <h4>Parallax scrolling children</h4>
- * Child views can opt to be scrolled within this layout in a parallax fashion.
- * See {@link LayoutParams#COLLAPSE_MODE_PARALLAX} and
- * {@link LayoutParams#setParallaxMultiplier(float)}.
- *
- * <h4>Pinned position children</h4>
- * Child views can opt to be pinned in space globally. This is useful when implementing a
- * collapsing as it allows the {@link Toolbar} to be fixed in place even though this layout is
- * moving. See {@link LayoutParams#COLLAPSE_MODE_PIN}.
- *
- * <p><strong>Do not manually add views to the Toolbar at run time</strong>.
- * We will add a 'dummy view' to the Toolbar which allows us to work out the available space
- * for the title. This can interfere with any views which you add.</p>
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleTextAppearance
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleTextAppearance
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_contentScrim
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMargin
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_statusBarScrim
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_toolbarId
- */
-public class CollapsingToolbarLayout extends FrameLayout {
-
- private static final int DEFAULT_SCRIM_ANIMATION_DURATION = 600;
-
- private boolean mRefreshToolbar = true;
- private int mToolbarId;
- private Toolbar mToolbar;
- private View mToolbarDirectChild;
- private View mDummyView;
-
- private int mExpandedMarginStart;
- private int mExpandedMarginTop;
- private int mExpandedMarginEnd;
- private int mExpandedMarginBottom;
-
- private final Rect mTmpRect = new Rect();
- final CollapsingTextHelper mCollapsingTextHelper;
- private boolean mCollapsingTitleEnabled;
- private boolean mDrawCollapsingTitle;
-
- private Drawable mContentScrim;
- Drawable mStatusBarScrim;
- private int mScrimAlpha;
- private boolean mScrimsAreShown;
- private ValueAnimator mScrimAnimator;
- private long mScrimAnimationDuration;
- private int mScrimVisibleHeightTrigger = -1;
-
- private AppBarLayout.OnOffsetChangedListener mOnOffsetChangedListener;
-
- int mCurrentOffset;
-
- WindowInsetsCompat mLastInsets;
-
- public CollapsingToolbarLayout(Context context) {
- this(context, null);
- }
-
- public CollapsingToolbarLayout(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public CollapsingToolbarLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- ThemeUtils.checkAppCompatTheme(context);
-
- mCollapsingTextHelper = new CollapsingTextHelper(this);
- mCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);
-
- TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.CollapsingToolbarLayout, defStyleAttr,
- R.style.Widget_Design_CollapsingToolbar);
-
- mCollapsingTextHelper.setExpandedTextGravity(
- a.getInt(R.styleable.CollapsingToolbarLayout_expandedTitleGravity,
- GravityCompat.START | Gravity.BOTTOM));
- mCollapsingTextHelper.setCollapsedTextGravity(
- a.getInt(R.styleable.CollapsingToolbarLayout_collapsedTitleGravity,
- GravityCompat.START | Gravity.CENTER_VERTICAL));
-
- mExpandedMarginStart = mExpandedMarginTop = mExpandedMarginEnd = mExpandedMarginBottom =
- a.getDimensionPixelSize(R.styleable.CollapsingToolbarLayout_expandedTitleMargin, 0);
-
- if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginStart)) {
- mExpandedMarginStart = a.getDimensionPixelSize(
- R.styleable.CollapsingToolbarLayout_expandedTitleMarginStart, 0);
- }
- if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginEnd)) {
- mExpandedMarginEnd = a.getDimensionPixelSize(
- R.styleable.CollapsingToolbarLayout_expandedTitleMarginEnd, 0);
- }
- if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginTop)) {
- mExpandedMarginTop = a.getDimensionPixelSize(
- R.styleable.CollapsingToolbarLayout_expandedTitleMarginTop, 0);
- }
- if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginBottom)) {
- mExpandedMarginBottom = a.getDimensionPixelSize(
- R.styleable.CollapsingToolbarLayout_expandedTitleMarginBottom, 0);
- }
-
- mCollapsingTitleEnabled = a.getBoolean(
- R.styleable.CollapsingToolbarLayout_titleEnabled, true);
- setTitle(a.getText(R.styleable.CollapsingToolbarLayout_title));
-
- // First load the default text appearances
- mCollapsingTextHelper.setExpandedTextAppearance(
- R.style.TextAppearance_Design_CollapsingToolbar_Expanded);
- mCollapsingTextHelper.setCollapsedTextAppearance(
- android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Widget_ActionBar_Title);
-
- // Now overlay any custom text appearances
- if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleTextAppearance)) {
- mCollapsingTextHelper.setExpandedTextAppearance(
- a.getResourceId(
- R.styleable.CollapsingToolbarLayout_expandedTitleTextAppearance, 0));
- }
- if (a.hasValue(R.styleable.CollapsingToolbarLayout_collapsedTitleTextAppearance)) {
- mCollapsingTextHelper.setCollapsedTextAppearance(
- a.getResourceId(
- R.styleable.CollapsingToolbarLayout_collapsedTitleTextAppearance, 0));
- }
-
- mScrimVisibleHeightTrigger = a.getDimensionPixelSize(
- R.styleable.CollapsingToolbarLayout_scrimVisibleHeightTrigger, -1);
-
- mScrimAnimationDuration = a.getInt(
- R.styleable.CollapsingToolbarLayout_scrimAnimationDuration,
- DEFAULT_SCRIM_ANIMATION_DURATION);
-
- setContentScrim(a.getDrawable(R.styleable.CollapsingToolbarLayout_contentScrim));
- setStatusBarScrim(a.getDrawable(R.styleable.CollapsingToolbarLayout_statusBarScrim));
-
- mToolbarId = a.getResourceId(R.styleable.CollapsingToolbarLayout_toolbarId, -1);
-
- a.recycle();
-
- setWillNotDraw(false);
-
- ViewCompat.setOnApplyWindowInsetsListener(this,
- new android.support.v4.view.OnApplyWindowInsetsListener() {
- @Override
- public WindowInsetsCompat onApplyWindowInsets(View v,
- WindowInsetsCompat insets) {
- return onWindowInsetChanged(insets);
- }
- });
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- // Add an OnOffsetChangedListener if possible
- final ViewParent parent = getParent();
- if (parent instanceof AppBarLayout) {
- // Copy over from the ABL whether we should fit system windows
- ViewCompat.setFitsSystemWindows(this, ViewCompat.getFitsSystemWindows((View) parent));
-
- if (mOnOffsetChangedListener == null) {
- mOnOffsetChangedListener = new OffsetUpdateListener();
- }
- ((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);
-
- // We're attached, so lets request an inset dispatch
- ViewCompat.requestApplyInsets(this);
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- // Remove our OnOffsetChangedListener if possible and it exists
- final ViewParent parent = getParent();
- if (mOnOffsetChangedListener != null && parent instanceof AppBarLayout) {
- ((AppBarLayout) parent).removeOnOffsetChangedListener(mOnOffsetChangedListener);
- }
-
- super.onDetachedFromWindow();
- }
-
- WindowInsetsCompat onWindowInsetChanged(final WindowInsetsCompat insets) {
- WindowInsetsCompat newInsets = null;
-
- if (ViewCompat.getFitsSystemWindows(this)) {
- // If we're set to fit system windows, keep the insets
- newInsets = insets;
- }
-
- // If our insets have changed, keep them and invalidate the scroll ranges...
- if (!ObjectsCompat.equals(mLastInsets, newInsets)) {
- mLastInsets = newInsets;
- requestLayout();
- }
-
- // Consume the insets. This is done so that child views with fitSystemWindows=true do not
- // get the default padding functionality from View
- return insets.consumeSystemWindowInsets();
- }
-
- @Override
- public void draw(Canvas canvas) {
- super.draw(canvas);
-
- // If we don't have a toolbar, the scrim will be not be drawn in drawChild() below.
- // Instead, we draw it here, before our collapsing text.
- ensureToolbar();
- if (mToolbar == null && mContentScrim != null && mScrimAlpha > 0) {
- mContentScrim.mutate().setAlpha(mScrimAlpha);
- mContentScrim.draw(canvas);
- }
-
- // Let the collapsing text helper draw its text
- if (mCollapsingTitleEnabled && mDrawCollapsingTitle) {
- mCollapsingTextHelper.draw(canvas);
- }
-
- // Now draw the status bar scrim
- if (mStatusBarScrim != null && mScrimAlpha > 0) {
- final int topInset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
- if (topInset > 0) {
- mStatusBarScrim.setBounds(0, -mCurrentOffset, getWidth(),
- topInset - mCurrentOffset);
- mStatusBarScrim.mutate().setAlpha(mScrimAlpha);
- mStatusBarScrim.draw(canvas);
- }
- }
- }
-
- @Override
- protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
- // This is a little weird. Our scrim needs to be behind the Toolbar (if it is present),
- // but in front of any other children which are behind it. To do this we intercept the
- // drawChild() call, and draw our scrim just before the Toolbar is drawn
- boolean invalidated = false;
- if (mContentScrim != null && mScrimAlpha > 0 && isToolbarChild(child)) {
- mContentScrim.mutate().setAlpha(mScrimAlpha);
- mContentScrim.draw(canvas);
- invalidated = true;
- }
- return super.drawChild(canvas, child, drawingTime) || invalidated;
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- if (mContentScrim != null) {
- mContentScrim.setBounds(0, 0, w, h);
- }
- }
-
- private void ensureToolbar() {
- if (!mRefreshToolbar) {
- return;
- }
-
- // First clear out the current Toolbar
- mToolbar = null;
- mToolbarDirectChild = null;
-
- if (mToolbarId != -1) {
- // If we have an ID set, try and find it and it's direct parent to us
- mToolbar = findViewById(mToolbarId);
- if (mToolbar != null) {
- mToolbarDirectChild = findDirectChild(mToolbar);
- }
- }
-
- if (mToolbar == null) {
- // If we don't have an ID, or couldn't find a Toolbar with the correct ID, try and find
- // one from our direct children
- Toolbar toolbar = null;
- for (int i = 0, count = getChildCount(); i < count; i++) {
- final View child = getChildAt(i);
- if (child instanceof Toolbar) {
- toolbar = (Toolbar) child;
- break;
- }
- }
- mToolbar = toolbar;
- }
-
- updateDummyView();
- mRefreshToolbar = false;
- }
-
- private boolean isToolbarChild(View child) {
- return (mToolbarDirectChild == null || mToolbarDirectChild == this)
- ? child == mToolbar
- : child == mToolbarDirectChild;
- }
-
- /**
- * Returns the direct child of this layout, which itself is the ancestor of the
- * given view.
- */
- private View findDirectChild(final View descendant) {
- View directChild = descendant;
- for (ViewParent p = descendant.getParent(); p != this && p != null; p = p.getParent()) {
- if (p instanceof View) {
- directChild = (View) p;
- }
- }
- return directChild;
- }
-
- private void updateDummyView() {
- if (!mCollapsingTitleEnabled && mDummyView != null) {
- // If we have a dummy view and we have our title disabled, remove it from its parent
- final ViewParent parent = mDummyView.getParent();
- if (parent instanceof ViewGroup) {
- ((ViewGroup) parent).removeView(mDummyView);
- }
- }
- if (mCollapsingTitleEnabled && mToolbar != null) {
- if (mDummyView == null) {
- mDummyView = new View(getContext());
- }
- if (mDummyView.getParent() == null) {
- mToolbar.addView(mDummyView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
- }
- }
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- ensureToolbar();
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- final int mode = MeasureSpec.getMode(heightMeasureSpec);
- final int topInset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
- if (mode == MeasureSpec.UNSPECIFIED && topInset > 0) {
- // If we have a top inset and we're set to wrap_content height we need to make sure
- // we add the top inset to our height, therefore we re-measure
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(
- getMeasuredHeight() + topInset, MeasureSpec.EXACTLY);
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
-
- if (mLastInsets != null) {
- // Shift down any views which are not set to fit system windows
- final int insetTop = mLastInsets.getSystemWindowInsetTop();
- for (int i = 0, z = getChildCount(); i < z; i++) {
- final View child = getChildAt(i);
- if (!ViewCompat.getFitsSystemWindows(child)) {
- if (child.getTop() < insetTop) {
- // If the child isn't set to fit system windows but is drawing within
- // the inset offset it down
- ViewCompat.offsetTopAndBottom(child, insetTop);
- }
- }
- }
- }
-
- // Update the collapsed bounds by getting it's transformed bounds
- if (mCollapsingTitleEnabled && mDummyView != null) {
- // We only draw the title if the dummy view is being displayed (Toolbar removes
- // views if there is no space)
- mDrawCollapsingTitle = ViewCompat.isAttachedToWindow(mDummyView)
- && mDummyView.getVisibility() == VISIBLE;
-
- if (mDrawCollapsingTitle) {
- final boolean isRtl = ViewCompat.getLayoutDirection(this)
- == ViewCompat.LAYOUT_DIRECTION_RTL;
-
- // Update the collapsed bounds
- final int maxOffset = getMaxOffsetForPinChild(
- mToolbarDirectChild != null ? mToolbarDirectChild : mToolbar);
- ViewGroupUtils.getDescendantRect(this, mDummyView, mTmpRect);
- mCollapsingTextHelper.setCollapsedBounds(
- mTmpRect.left + (isRtl
- ? mToolbar.getTitleMarginEnd()
- : mToolbar.getTitleMarginStart()),
- mTmpRect.top + maxOffset + mToolbar.getTitleMarginTop(),
- mTmpRect.right + (isRtl
- ? mToolbar.getTitleMarginStart()
- : mToolbar.getTitleMarginEnd()),
- mTmpRect.bottom + maxOffset - mToolbar.getTitleMarginBottom());
-
- // Update the expanded bounds
- mCollapsingTextHelper.setExpandedBounds(
- isRtl ? mExpandedMarginEnd : mExpandedMarginStart,
- mTmpRect.top + mExpandedMarginTop,
- right - left - (isRtl ? mExpandedMarginStart : mExpandedMarginEnd),
- bottom - top - mExpandedMarginBottom);
- // Now recalculate using the new bounds
- mCollapsingTextHelper.recalculate();
- }
- }
-
- // Update our child view offset helpers. This needs to be done after the title has been
- // setup, so that any Toolbars are in their original position
- for (int i = 0, z = getChildCount(); i < z; i++) {
- getViewOffsetHelper(getChildAt(i)).onViewLayout();
- }
-
- // Finally, set our minimum height to enable proper AppBarLayout collapsing
- if (mToolbar != null) {
- if (mCollapsingTitleEnabled && TextUtils.isEmpty(mCollapsingTextHelper.getText())) {
- // If we do not currently have a title, try and grab it from the Toolbar
- mCollapsingTextHelper.setText(mToolbar.getTitle());
- }
- if (mToolbarDirectChild == null || mToolbarDirectChild == this) {
- setMinimumHeight(getHeightWithMargins(mToolbar));
- } else {
- setMinimumHeight(getHeightWithMargins(mToolbarDirectChild));
- }
- }
-
- updateScrimVisibility();
- }
-
- private static int getHeightWithMargins(@NonNull final View view) {
- final ViewGroup.LayoutParams lp = view.getLayoutParams();
- if (lp instanceof MarginLayoutParams) {
- final MarginLayoutParams mlp = (MarginLayoutParams) lp;
- return view.getHeight() + mlp.topMargin + mlp.bottomMargin;
- }
- return view.getHeight();
- }
-
- static ViewOffsetHelper getViewOffsetHelper(View view) {
- ViewOffsetHelper offsetHelper = (ViewOffsetHelper) view.getTag(R.id.view_offset_helper);
- if (offsetHelper == null) {
- offsetHelper = new ViewOffsetHelper(view);
- view.setTag(R.id.view_offset_helper, offsetHelper);
- }
- return offsetHelper;
- }
-
- /**
- * Sets the title to be displayed by this view, if enabled.
- *
- * @see #setTitleEnabled(boolean)
- * @see #getTitle()
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_title
- */
- public void setTitle(@Nullable CharSequence title) {
- mCollapsingTextHelper.setText(title);
- }
-
- /**
- * Returns the title currently being displayed by this view. If the title is not enabled, then
- * this will return {@code null}.
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_title
- */
- @Nullable
- public CharSequence getTitle() {
- return mCollapsingTitleEnabled ? mCollapsingTextHelper.getText() : null;
- }
-
- /**
- * Sets whether this view should display its own title.
- *
- * <p>The title displayed by this view will shrink and grow based on the scroll offset.</p>
- *
- * @see #setTitle(CharSequence)
- * @see #isTitleEnabled()
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_titleEnabled
- */
- public void setTitleEnabled(boolean enabled) {
- if (enabled != mCollapsingTitleEnabled) {
- mCollapsingTitleEnabled = enabled;
- updateDummyView();
- requestLayout();
- }
- }
-
- /**
- * Returns whether this view is currently displaying its own title.
- *
- * @see #setTitleEnabled(boolean)
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_titleEnabled
- */
- public boolean isTitleEnabled() {
- return mCollapsingTitleEnabled;
- }
-
- /**
- * Set whether the content scrim and/or status bar scrim should be shown or not. Any change
- * in the vertical scroll may overwrite this value. Any visibility change will be animated if
- * this view has already been laid out.
- *
- * @param shown whether the scrims should be shown
- *
- * @see #getStatusBarScrim()
- * @see #getContentScrim()
- */
- public void setScrimsShown(boolean shown) {
- setScrimsShown(shown, ViewCompat.isLaidOut(this) && !isInEditMode());
- }
-
- /**
- * Set whether the content scrim and/or status bar scrim should be shown or not. Any change
- * in the vertical scroll may overwrite this value.
- *
- * @param shown whether the scrims should be shown
- * @param animate whether to animate the visibility change
- *
- * @see #getStatusBarScrim()
- * @see #getContentScrim()
- */
- public void setScrimsShown(boolean shown, boolean animate) {
- if (mScrimsAreShown != shown) {
- if (animate) {
- animateScrim(shown ? 0xFF : 0x0);
- } else {
- setScrimAlpha(shown ? 0xFF : 0x0);
- }
- mScrimsAreShown = shown;
- }
- }
-
- private void animateScrim(int targetAlpha) {
- ensureToolbar();
- if (mScrimAnimator == null) {
- mScrimAnimator = new ValueAnimator();
- mScrimAnimator.setDuration(mScrimAnimationDuration);
- mScrimAnimator.setInterpolator(
- targetAlpha > mScrimAlpha
- ? AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR
- : AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
- mScrimAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- setScrimAlpha((int) animator.getAnimatedValue());
- }
- });
- } else if (mScrimAnimator.isRunning()) {
- mScrimAnimator.cancel();
- }
-
- mScrimAnimator.setIntValues(mScrimAlpha, targetAlpha);
- mScrimAnimator.start();
- }
-
- void setScrimAlpha(int alpha) {
- if (alpha != mScrimAlpha) {
- final Drawable contentScrim = mContentScrim;
- if (contentScrim != null && mToolbar != null) {
- ViewCompat.postInvalidateOnAnimation(mToolbar);
- }
- mScrimAlpha = alpha;
- ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this);
- }
- }
-
- int getScrimAlpha() {
- return mScrimAlpha;
- }
-
- /**
- * Set the drawable to use for the content scrim from resources. Providing null will disable
- * the scrim functionality.
- *
- * @param drawable the drawable to display
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim
- * @see #getContentScrim()
- */
- public void setContentScrim(@Nullable Drawable drawable) {
- if (mContentScrim != drawable) {
- if (mContentScrim != null) {
- mContentScrim.setCallback(null);
- }
- mContentScrim = drawable != null ? drawable.mutate() : null;
- if (mContentScrim != null) {
- mContentScrim.setBounds(0, 0, getWidth(), getHeight());
- mContentScrim.setCallback(this);
- mContentScrim.setAlpha(mScrimAlpha);
- }
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- /**
- * Set the color to use for the content scrim.
- *
- * @param color the color to display
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim
- * @see #getContentScrim()
- */
- public void setContentScrimColor(@ColorInt int color) {
- setContentScrim(new ColorDrawable(color));
- }
-
- /**
- * Set the drawable to use for the content scrim from resources.
- *
- * @param resId drawable resource id
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim
- * @see #getContentScrim()
- */
- public void setContentScrimResource(@DrawableRes int resId) {
- setContentScrim(ContextCompat.getDrawable(getContext(), resId));
-
- }
-
- /**
- * Returns the drawable which is used for the foreground scrim.
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim
- * @see #setContentScrim(Drawable)
- */
- @Nullable
- public Drawable getContentScrim() {
- return mContentScrim;
- }
-
- /**
- * Set the drawable to use for the status bar scrim from resources.
- * Providing null will disable the scrim functionality.
- *
- * <p>This scrim is only shown when we have been given a top system inset.</p>
- *
- * @param drawable the drawable to display
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim
- * @see #getStatusBarScrim()
- */
- public void setStatusBarScrim(@Nullable Drawable drawable) {
- if (mStatusBarScrim != drawable) {
- if (mStatusBarScrim != null) {
- mStatusBarScrim.setCallback(null);
- }
- mStatusBarScrim = drawable != null ? drawable.mutate() : null;
- if (mStatusBarScrim != null) {
- if (mStatusBarScrim.isStateful()) {
- mStatusBarScrim.setState(getDrawableState());
- }
- DrawableCompat.setLayoutDirection(mStatusBarScrim,
- ViewCompat.getLayoutDirection(this));
- mStatusBarScrim.setVisible(getVisibility() == VISIBLE, false);
- mStatusBarScrim.setCallback(this);
- mStatusBarScrim.setAlpha(mScrimAlpha);
- }
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- @Override
- protected void drawableStateChanged() {
- super.drawableStateChanged();
-
- final int[] state = getDrawableState();
- boolean changed = false;
-
- Drawable d = mStatusBarScrim;
- if (d != null && d.isStateful()) {
- changed |= d.setState(state);
- }
- d = mContentScrim;
- if (d != null && d.isStateful()) {
- changed |= d.setState(state);
- }
- if (mCollapsingTextHelper != null) {
- changed |= mCollapsingTextHelper.setState(state);
- }
-
- if (changed) {
- invalidate();
- }
- }
-
- @Override
- protected boolean verifyDrawable(Drawable who) {
- return super.verifyDrawable(who) || who == mContentScrim || who == mStatusBarScrim;
- }
-
- @Override
- public void setVisibility(int visibility) {
- super.setVisibility(visibility);
-
- final boolean visible = visibility == VISIBLE;
- if (mStatusBarScrim != null && mStatusBarScrim.isVisible() != visible) {
- mStatusBarScrim.setVisible(visible, false);
- }
- if (mContentScrim != null && mContentScrim.isVisible() != visible) {
- mContentScrim.setVisible(visible, false);
- }
- }
-
- /**
- * Set the color to use for the status bar scrim.
- *
- * <p>This scrim is only shown when we have been given a top system inset.</p>
- *
- * @param color the color to display
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim
- * @see #getStatusBarScrim()
- */
- public void setStatusBarScrimColor(@ColorInt int color) {
- setStatusBarScrim(new ColorDrawable(color));
- }
-
- /**
- * Set the drawable to use for the content scrim from resources.
- *
- * @param resId drawable resource id
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim
- * @see #getStatusBarScrim()
- */
- public void setStatusBarScrimResource(@DrawableRes int resId) {
- setStatusBarScrim(ContextCompat.getDrawable(getContext(), resId));
- }
-
- /**
- * Returns the drawable which is used for the status bar scrim.
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim
- * @see #setStatusBarScrim(Drawable)
- */
- @Nullable
- public Drawable getStatusBarScrim() {
- return mStatusBarScrim;
- }
-
- /**
- * Sets the text color and size for the collapsed title from the specified
- * TextAppearance resource.
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleTextAppearance
- */
- public void setCollapsedTitleTextAppearance(@StyleRes int resId) {
- mCollapsingTextHelper.setCollapsedTextAppearance(resId);
- }
-
- /**
- * Sets the text color of the collapsed title.
- *
- * @param color The new text color in ARGB format
- */
- public void setCollapsedTitleTextColor(@ColorInt int color) {
- setCollapsedTitleTextColor(ColorStateList.valueOf(color));
- }
-
- /**
- * Sets the text colors of the collapsed title.
- *
- * @param colors ColorStateList containing the new text colors
- */
- public void setCollapsedTitleTextColor(@NonNull ColorStateList colors) {
- mCollapsingTextHelper.setCollapsedTextColor(colors);
- }
-
- /**
- * Sets the horizontal alignment of the collapsed title and the vertical gravity that will
- * be used when there is extra space in the collapsed bounds beyond what is required for
- * the title itself.
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleGravity
- */
- public void setCollapsedTitleGravity(int gravity) {
- mCollapsingTextHelper.setCollapsedTextGravity(gravity);
- }
-
- /**
- * Returns the horizontal and vertical alignment for title when collapsed.
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleGravity
- */
- public int getCollapsedTitleGravity() {
- return mCollapsingTextHelper.getCollapsedTextGravity();
- }
-
- /**
- * Sets the text color and size for the expanded title from the specified
- * TextAppearance resource.
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleTextAppearance
- */
- public void setExpandedTitleTextAppearance(@StyleRes int resId) {
- mCollapsingTextHelper.setExpandedTextAppearance(resId);
- }
-
- /**
- * Sets the text color of the expanded title.
- *
- * @param color The new text color in ARGB format
- */
- public void setExpandedTitleColor(@ColorInt int color) {
- setExpandedTitleTextColor(ColorStateList.valueOf(color));
- }
-
- /**
- * Sets the text colors of the expanded title.
- *
- * @param colors ColorStateList containing the new text colors
- */
- public void setExpandedTitleTextColor(@NonNull ColorStateList colors) {
- mCollapsingTextHelper.setExpandedTextColor(colors);
- }
-
- /**
- * Sets the horizontal alignment of the expanded title and the vertical gravity that will
- * be used when there is extra space in the expanded bounds beyond what is required for
- * the title itself.
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleGravity
- */
- public void setExpandedTitleGravity(int gravity) {
- mCollapsingTextHelper.setExpandedTextGravity(gravity);
- }
-
- /**
- * Returns the horizontal and vertical alignment for title when expanded.
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleGravity
- */
- public int getExpandedTitleGravity() {
- return mCollapsingTextHelper.getExpandedTextGravity();
- }
-
- /**
- * Set the typeface to use for the collapsed title.
- *
- * @param typeface typeface to use, or {@code null} to use the default.
- */
- public void setCollapsedTitleTypeface(@Nullable Typeface typeface) {
- mCollapsingTextHelper.setCollapsedTypeface(typeface);
- }
-
- /**
- * Returns the typeface used for the collapsed title.
- */
- @NonNull
- public Typeface getCollapsedTitleTypeface() {
- return mCollapsingTextHelper.getCollapsedTypeface();
- }
-
- /**
- * Set the typeface to use for the expanded title.
- *
- * @param typeface typeface to use, or {@code null} to use the default.
- */
- public void setExpandedTitleTypeface(@Nullable Typeface typeface) {
- mCollapsingTextHelper.setExpandedTypeface(typeface);
- }
-
- /**
- * Returns the typeface used for the expanded title.
- */
- @NonNull
- public Typeface getExpandedTitleTypeface() {
- return mCollapsingTextHelper.getExpandedTypeface();
- }
-
- /**
- * Sets the expanded title margins.
- *
- * @param start the starting title margin in pixels
- * @param top the top title margin in pixels
- * @param end the ending title margin in pixels
- * @param bottom the bottom title margin in pixels
- *
- * @see #getExpandedTitleMarginStart()
- * @see #getExpandedTitleMarginTop()
- * @see #getExpandedTitleMarginEnd()
- * @see #getExpandedTitleMarginBottom()
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMargin
- */
- public void setExpandedTitleMargin(int start, int top, int end, int bottom) {
- mExpandedMarginStart = start;
- mExpandedMarginTop = top;
- mExpandedMarginEnd = end;
- mExpandedMarginBottom = bottom;
- requestLayout();
- }
-
- /**
- * @return the starting expanded title margin in pixels
- *
- * @see #setExpandedTitleMarginStart(int)
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart
- */
- public int getExpandedTitleMarginStart() {
- return mExpandedMarginStart;
- }
-
- /**
- * Sets the starting expanded title margin in pixels.
- *
- * @param margin the starting title margin in pixels
- * @see #getExpandedTitleMarginStart()
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart
- */
- public void setExpandedTitleMarginStart(int margin) {
- mExpandedMarginStart = margin;
- requestLayout();
- }
-
- /**
- * @return the top expanded title margin in pixels
- * @see #setExpandedTitleMarginTop(int)
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginTop
- */
- public int getExpandedTitleMarginTop() {
- return mExpandedMarginTop;
- }
-
- /**
- * Sets the top expanded title margin in pixels.
- *
- * @param margin the top title margin in pixels
- * @see #getExpandedTitleMarginTop()
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginTop
- */
- public void setExpandedTitleMarginTop(int margin) {
- mExpandedMarginTop = margin;
- requestLayout();
- }
-
- /**
- * @return the ending expanded title margin in pixels
- * @see #setExpandedTitleMarginEnd(int)
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd
- */
- public int getExpandedTitleMarginEnd() {
- return mExpandedMarginEnd;
- }
-
- /**
- * Sets the ending expanded title margin in pixels.
- *
- * @param margin the ending title margin in pixels
- * @see #getExpandedTitleMarginEnd()
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd
- */
- public void setExpandedTitleMarginEnd(int margin) {
- mExpandedMarginEnd = margin;
- requestLayout();
- }
-
- /**
- * @return the bottom expanded title margin in pixels
- * @see #setExpandedTitleMarginBottom(int)
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom
- */
- public int getExpandedTitleMarginBottom() {
- return mExpandedMarginBottom;
- }
-
- /**
- * Sets the bottom expanded title margin in pixels.
- *
- * @param margin the bottom title margin in pixels
- * @see #getExpandedTitleMarginBottom()
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom
- */
- public void setExpandedTitleMarginBottom(int margin) {
- mExpandedMarginBottom = margin;
- requestLayout();
- }
-
- /**
- * Set the amount of visible height in pixels used to define when to trigger a scrim
- * visibility change.
- *
- * <p>If the visible height of this view is less than the given value, the scrims will be
- * made visible, otherwise they are hidden.</p>
- *
- * @param height value in pixels used to define when to trigger a scrim visibility change
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_scrimVisibleHeightTrigger
- */
- public void setScrimVisibleHeightTrigger(@IntRange(from = 0) final int height) {
- if (mScrimVisibleHeightTrigger != height) {
- mScrimVisibleHeightTrigger = height;
- // Update the scrim visibility
- updateScrimVisibility();
- }
- }
-
- /**
- * Returns the amount of visible height in pixels used to define when to trigger a scrim
- * visibility change.
- *
- * @see #setScrimVisibleHeightTrigger(int)
- */
- public int getScrimVisibleHeightTrigger() {
- if (mScrimVisibleHeightTrigger >= 0) {
- // If we have one explicitly set, return it
- return mScrimVisibleHeightTrigger;
- }
-
- // Otherwise we'll use the default computed value
- final int insetTop = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
-
- final int minHeight = ViewCompat.getMinimumHeight(this);
- if (minHeight > 0) {
- // If we have a minHeight set, lets use 2 * minHeight (capped at our height)
- return Math.min((minHeight * 2) + insetTop, getHeight());
- }
-
- // If we reach here then we don't have a min height set. Instead we'll take a
- // guess at 1/3 of our height being visible
- return getHeight() / 3;
- }
-
- /**
- * Set the duration used for scrim visibility animations.
- *
- * @param duration the duration to use in milliseconds
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_scrimAnimationDuration
- */
- public void setScrimAnimationDuration(@IntRange(from = 0) final long duration) {
- mScrimAnimationDuration = duration;
- }
-
- /**
- * Returns the duration in milliseconds used for scrim visibility animations.
- */
- public long getScrimAnimationDuration() {
- return mScrimAnimationDuration;
- }
-
- @Override
- protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
- return p instanceof LayoutParams;
- }
-
- @Override
- protected LayoutParams generateDefaultLayoutParams() {
- return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
- }
-
- @Override
- public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
- return new LayoutParams(getContext(), attrs);
- }
-
- @Override
- protected FrameLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
- return new LayoutParams(p);
- }
-
- public static class LayoutParams extends FrameLayout.LayoutParams {
-
- private static final float DEFAULT_PARALLAX_MULTIPLIER = 0.5f;
-
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef({
- COLLAPSE_MODE_OFF,
- COLLAPSE_MODE_PIN,
- COLLAPSE_MODE_PARALLAX
- })
- @Retention(RetentionPolicy.SOURCE)
- @interface CollapseMode {}
-
- /**
- * The view will act as normal with no collapsing behavior.
- */
- public static final int COLLAPSE_MODE_OFF = 0;
-
- /**
- * The view will pin in place until it reaches the bottom of the
- * {@link CollapsingToolbarLayout}.
- */
- public static final int COLLAPSE_MODE_PIN = 1;
-
- /**
- * The view will scroll in a parallax fashion. See {@link #setParallaxMultiplier(float)}
- * to change the multiplier used.
- */
- public static final int COLLAPSE_MODE_PARALLAX = 2;
-
- int mCollapseMode = COLLAPSE_MODE_OFF;
- float mParallaxMult = DEFAULT_PARALLAX_MULTIPLIER;
-
- public LayoutParams(Context c, AttributeSet attrs) {
- super(c, attrs);
-
- TypedArray a = c.obtainStyledAttributes(attrs,
- R.styleable.CollapsingToolbarLayout_Layout);
- mCollapseMode = a.getInt(
- R.styleable.CollapsingToolbarLayout_Layout_layout_collapseMode,
- COLLAPSE_MODE_OFF);
- setParallaxMultiplier(a.getFloat(
- R.styleable.CollapsingToolbarLayout_Layout_layout_collapseParallaxMultiplier,
- DEFAULT_PARALLAX_MULTIPLIER));
- a.recycle();
- }
-
- public LayoutParams(int width, int height) {
- super(width, height);
- }
-
- public LayoutParams(int width, int height, int gravity) {
- super(width, height, gravity);
- }
-
- public LayoutParams(ViewGroup.LayoutParams p) {
- super(p);
- }
-
- public LayoutParams(MarginLayoutParams source) {
- super(source);
- }
-
- @RequiresApi(19)
- public LayoutParams(FrameLayout.LayoutParams source) {
- // The copy constructor called here only exists on API 19+.
- super(source);
- }
-
- /**
- * Set the collapse mode.
- *
- * @param collapseMode one of {@link #COLLAPSE_MODE_OFF}, {@link #COLLAPSE_MODE_PIN}
- * or {@link #COLLAPSE_MODE_PARALLAX}.
- */
- public void setCollapseMode(@CollapseMode int collapseMode) {
- mCollapseMode = collapseMode;
- }
-
- /**
- * Returns the requested collapse mode.
- *
- * @return the current mode. One of {@link #COLLAPSE_MODE_OFF}, {@link #COLLAPSE_MODE_PIN}
- * or {@link #COLLAPSE_MODE_PARALLAX}.
- */
- @CollapseMode
- public int getCollapseMode() {
- return mCollapseMode;
- }
-
- /**
- * Set the parallax scroll multiplier used in conjunction with
- * {@link #COLLAPSE_MODE_PARALLAX}. A value of {@code 0.0} indicates no movement at all,
- * {@code 1.0f} indicates normal scroll movement.
- *
- * @param multiplier the multiplier.
- *
- * @see #getParallaxMultiplier()
- */
- public void setParallaxMultiplier(float multiplier) {
- mParallaxMult = multiplier;
- }
-
- /**
- * Returns the parallax scroll multiplier used in conjunction with
- * {@link #COLLAPSE_MODE_PARALLAX}.
- *
- * @see #setParallaxMultiplier(float)
- */
- public float getParallaxMultiplier() {
- return mParallaxMult;
- }
- }
-
- /**
- * Show or hide the scrims if needed
- */
- final void updateScrimVisibility() {
- if (mContentScrim != null || mStatusBarScrim != null) {
- setScrimsShown(getHeight() + mCurrentOffset < getScrimVisibleHeightTrigger());
- }
- }
-
- final int getMaxOffsetForPinChild(View child) {
- final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- return getHeight()
- - offsetHelper.getLayoutTop()
- - child.getHeight()
- - lp.bottomMargin;
- }
-
- private class OffsetUpdateListener implements AppBarLayout.OnOffsetChangedListener {
- OffsetUpdateListener() {
- }
-
- @Override
- public void onOffsetChanged(AppBarLayout layout, int verticalOffset) {
- mCurrentOffset = verticalOffset;
-
- final int insetTop = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
-
- for (int i = 0, z = getChildCount(); i < z; i++) {
- final View child = getChildAt(i);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);
-
- switch (lp.mCollapseMode) {
- case LayoutParams.COLLAPSE_MODE_PIN:
- offsetHelper.setTopAndBottomOffset(MathUtils.clamp(
- -verticalOffset, 0, getMaxOffsetForPinChild(child)));
- break;
- case LayoutParams.COLLAPSE_MODE_PARALLAX:
- offsetHelper.setTopAndBottomOffset(
- Math.round(-verticalOffset * lp.mParallaxMult));
- break;
- }
- }
-
- // Show or hide the scrims if needed
- updateScrimVisibility();
-
- if (mStatusBarScrim != null && insetTop > 0) {
- ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this);
- }
-
- // Update the collapsing text's fraction
- final int expandRange = getHeight() - ViewCompat.getMinimumHeight(
- CollapsingToolbarLayout.this) - insetTop;
- mCollapsingTextHelper.setExpansionFraction(
- Math.abs(verticalOffset) / (float) expandRange);
- }
- }
-}
diff --git a/android/support/design/widget/CoordinatorLayout.java b/android/support/design/widget/CoordinatorLayout.java
index 03cce02..b7f47f4 100644
--- a/android/support/design/widget/CoordinatorLayout.java
+++ b/android/support/design/widget/CoordinatorLayout.java
@@ -366,7 +366,11 @@
return insets;
}
- final WindowInsetsCompat getLastWindowInsets() {
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public final WindowInsetsCompat getLastWindowInsets() {
return mLastInsets;
}
diff --git a/android/support/design/widget/DrawableUtils.java b/android/support/design/widget/DrawableUtils.java
deleted file mode 100644
index df1c04b..0000000
--- a/android/support/design/widget/DrawableUtils.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.DrawableContainer;
-import android.util.Log;
-
-import java.lang.reflect.Method;
-
-/**
- * Caution. Gross hacks ahead.
- */
-class DrawableUtils {
-
- private static final String LOG_TAG = "DrawableUtils";
-
- private static Method sSetConstantStateMethod;
- private static boolean sSetConstantStateMethodFetched;
-
- private DrawableUtils() {}
-
- static boolean setContainerConstantState(DrawableContainer drawable,
- Drawable.ConstantState constantState) {
- // We can use getDeclaredMethod() on v9+
- return setContainerConstantStateV9(drawable, constantState);
- }
-
- private static boolean setContainerConstantStateV9(DrawableContainer drawable,
- Drawable.ConstantState constantState) {
- if (!sSetConstantStateMethodFetched) {
- try {
- sSetConstantStateMethod = DrawableContainer.class.getDeclaredMethod(
- "setConstantState", DrawableContainer.DrawableContainerState.class);
- sSetConstantStateMethod.setAccessible(true);
- } catch (NoSuchMethodException e) {
- Log.e(LOG_TAG, "Could not fetch setConstantState(). Oh well.");
- }
- sSetConstantStateMethodFetched = true;
- }
- if (sSetConstantStateMethod != null) {
- try {
- sSetConstantStateMethod.invoke(drawable, constantState);
- return true;
- } catch (Exception e) {
- Log.e(LOG_TAG, "Could not invoke setConstantState(). Oh well.");
- }
- }
- return false;
- }
-}
diff --git a/android/support/design/widget/FloatingActionButton.java b/android/support/design/widget/FloatingActionButton.java
deleted file mode 100644
index f37b379..0000000
--- a/android/support/design/widget/FloatingActionButton.java
+++ /dev/null
@@ -1,870 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.support.annotation.ColorInt;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.VisibleForTesting;
-import android.support.design.R;
-import android.support.design.widget.FloatingActionButtonImpl.InternalVisibilityChangedListener;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.widget.ViewGroupUtils;
-import android.support.v7.widget.AppCompatImageHelper;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.Gravity;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.List;
-
-/**
- * Floating action buttons are used for a special type of promoted action. They are distinguished
- * by a circled icon floating above the UI and have special motion behaviors related to morphing,
- * launching, and the transferring anchor point.
- *
- * <p>Floating action buttons come in two sizes: the default and the mini. The size can be
- * controlled with the {@code fabSize} attribute.</p>
- *
- * <p>As this class descends from {@link ImageView}, you can control the icon which is displayed
- * via {@link #setImageDrawable(Drawable)}.</p>
- *
- * <p>The background color of this view defaults to the your theme's {@code colorAccent}. If you
- * wish to change this at runtime then you can do so via
- * {@link #setBackgroundTintList(ColorStateList)}.</p>
- */
[email protected](FloatingActionButton.Behavior.class)
-public class FloatingActionButton extends VisibilityAwareImageButton {
-
- private static final String LOG_TAG = "FloatingActionButton";
-
- /**
- * Callback to be invoked when the visibility of a FloatingActionButton changes.
- */
- public abstract static class OnVisibilityChangedListener {
- /**
- * Called when a FloatingActionButton has been
- * {@link #show(OnVisibilityChangedListener) shown}.
- *
- * @param fab the FloatingActionButton that was shown.
- */
- public void onShown(FloatingActionButton fab) {}
-
- /**
- * Called when a FloatingActionButton has been
- * {@link #hide(OnVisibilityChangedListener) hidden}.
- *
- * @param fab the FloatingActionButton that was hidden.
- */
- public void onHidden(FloatingActionButton fab) {}
- }
-
- // These values must match those in the attrs declaration
-
- /**
- * The mini sized button. Will always been smaller than {@link #SIZE_NORMAL}.
- *
- * @see #setSize(int)
- */
- public static final int SIZE_MINI = 1;
-
- /**
- * The normal sized button. Will always been larger than {@link #SIZE_MINI}.
- *
- * @see #setSize(int)
- */
- public static final int SIZE_NORMAL = 0;
-
- /**
- * Size which will change based on the window size. For small sized windows
- * (largest screen dimension < 470dp) this will select a small sized button, and for
- * larger sized windows it will select a larger size.
- *
- * @see #setSize(int)
- */
- public static final int SIZE_AUTO = -1;
-
- /**
- * Indicates that FloatingActionButton should not have a custom size.
- */
- public static final int NO_CUSTOM_SIZE = 0;
-
- /**
- * The switch point for the largest screen edge where SIZE_AUTO switches from mini to normal.
- */
- private static final int AUTO_MINI_LARGEST_SCREEN_WIDTH = 470;
-
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({SIZE_MINI, SIZE_NORMAL, SIZE_AUTO})
- public @interface Size {}
-
- private ColorStateList mBackgroundTint;
- private PorterDuff.Mode mBackgroundTintMode;
-
- private int mBorderWidth;
- private int mRippleColor;
- private int mSize;
- private int mCustomSize;
- int mImagePadding;
- private int mMaxImageSize;
-
- boolean mCompatPadding;
- final Rect mShadowPadding = new Rect();
- private final Rect mTouchArea = new Rect();
-
- private AppCompatImageHelper mImageHelper;
-
- private FloatingActionButtonImpl mImpl;
-
- public FloatingActionButton(Context context) {
- this(context, null);
- }
-
- public FloatingActionButton(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- ThemeUtils.checkAppCompatTheme(context);
-
- TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.FloatingActionButton, defStyleAttr,
- R.style.Widget_Design_FloatingActionButton);
- mBackgroundTint = a.getColorStateList(R.styleable.FloatingActionButton_backgroundTint);
- mBackgroundTintMode = ViewUtils.parseTintMode(a.getInt(
- R.styleable.FloatingActionButton_backgroundTintMode, -1), null);
- mRippleColor = a.getColor(R.styleable.FloatingActionButton_rippleColor, 0);
- mSize = a.getInt(R.styleable.FloatingActionButton_fabSize, SIZE_AUTO);
- mCustomSize = a.getDimensionPixelSize(R.styleable.FloatingActionButton_fabCustomSize,
- 0);
- mBorderWidth = a.getDimensionPixelSize(R.styleable.FloatingActionButton_borderWidth, 0);
- final float elevation = a.getDimension(R.styleable.FloatingActionButton_elevation, 0f);
- final float pressedTranslationZ = a.getDimension(
- R.styleable.FloatingActionButton_pressedTranslationZ, 0f);
- mCompatPadding = a.getBoolean(R.styleable.FloatingActionButton_useCompatPadding, false);
- a.recycle();
-
- mImageHelper = new AppCompatImageHelper(this);
- mImageHelper.loadFromAttributes(attrs, defStyleAttr);
-
- mMaxImageSize = (int) getResources().getDimension(R.dimen.design_fab_image_size);
-
- getImpl().setBackgroundDrawable(mBackgroundTint, mBackgroundTintMode,
- mRippleColor, mBorderWidth);
- getImpl().setElevation(elevation);
- getImpl().setPressedTranslationZ(pressedTranslationZ);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- final int preferredSize = getSizeDimension();
-
- mImagePadding = (preferredSize - mMaxImageSize) / 2;
- getImpl().updatePadding();
-
- final int w = resolveAdjustedSize(preferredSize, widthMeasureSpec);
- final int h = resolveAdjustedSize(preferredSize, heightMeasureSpec);
-
- // As we want to stay circular, we set both dimensions to be the
- // smallest resolved dimension
- final int d = Math.min(w, h);
-
- // We add the shadow's padding to the measured dimension
- setMeasuredDimension(
- d + mShadowPadding.left + mShadowPadding.right,
- d + mShadowPadding.top + mShadowPadding.bottom);
- }
-
- /**
- * Returns the ripple color for this button.
- *
- * @return the ARGB color used for the ripple
- * @see #setRippleColor(int)
- */
- @ColorInt
- public int getRippleColor() {
- return mRippleColor;
- }
-
- /**
- * Sets the ripple color for this button.
- *
- * <p>When running on devices with KitKat or below, we draw this color as a filled circle
- * rather than a ripple.</p>
- *
- * @param color ARGB color to use for the ripple
- * @attr ref android.support.design.R.styleable#FloatingActionButton_rippleColor
- * @see #getRippleColor()
- */
- public void setRippleColor(@ColorInt int color) {
- if (mRippleColor != color) {
- mRippleColor = color;
- getImpl().setRippleColor(color);
- }
- }
-
- /**
- * Returns the tint applied to the background drawable, if specified.
- *
- * @return the tint applied to the background drawable
- * @see #setBackgroundTintList(ColorStateList)
- */
- @Nullable
- @Override
- public ColorStateList getBackgroundTintList() {
- return mBackgroundTint;
- }
-
- /**
- * Applies a tint to the background drawable. Does not modify the current tint
- * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
- *
- * @param tint the tint to apply, may be {@code null} to clear tint
- */
- @Override
- public void setBackgroundTintList(@Nullable ColorStateList tint) {
- if (mBackgroundTint != tint) {
- mBackgroundTint = tint;
- getImpl().setBackgroundTintList(tint);
- }
- }
-
- /**
- * Returns the blending mode used to apply the tint to the background
- * drawable, if specified.
- *
- * @return the blending mode used to apply the tint to the background
- * drawable
- * @see #setBackgroundTintMode(PorterDuff.Mode)
- */
- @Nullable
- @Override
- public PorterDuff.Mode getBackgroundTintMode() {
- return mBackgroundTintMode;
- }
-
- /**
- * Specifies the blending mode used to apply the tint specified by
- * {@link #setBackgroundTintList(ColorStateList)}} to the background
- * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
- *
- * @param tintMode the blending mode used to apply the tint, may be
- * {@code null} to clear tint
- */
- @Override
- public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
- if (mBackgroundTintMode != tintMode) {
- mBackgroundTintMode = tintMode;
- getImpl().setBackgroundTintMode(tintMode);
- }
- }
-
- @Override
- public void setBackgroundDrawable(Drawable background) {
- Log.i(LOG_TAG, "Setting a custom background is not supported.");
- }
-
- @Override
- public void setBackgroundResource(int resid) {
- Log.i(LOG_TAG, "Setting a custom background is not supported.");
- }
-
- @Override
- public void setBackgroundColor(int color) {
- Log.i(LOG_TAG, "Setting a custom background is not supported.");
- }
-
- @Override
- public void setImageResource(@DrawableRes int resId) {
- // Intercept this call and instead retrieve the Drawable via the image helper
- mImageHelper.setImageResource(resId);
- }
-
- /**
- * Shows the button.
- * <p>This method will animate the button show if the view has already been laid out.</p>
- */
- public void show() {
- show(null);
- }
-
- /**
- * Shows the button.
- * <p>This method will animate the button show if the view has already been laid out.</p>
- *
- * @param listener the listener to notify when this view is shown
- */
- public void show(@Nullable final OnVisibilityChangedListener listener) {
- show(listener, true);
- }
-
- void show(OnVisibilityChangedListener listener, boolean fromUser) {
- getImpl().show(wrapOnVisibilityChangedListener(listener), fromUser);
- }
-
- /**
- * Hides the button.
- * <p>This method will animate the button hide if the view has already been laid out.</p>
- */
- public void hide() {
- hide(null);
- }
-
- /**
- * Hides the button.
- * <p>This method will animate the button hide if the view has already been laid out.</p>
- *
- * @param listener the listener to notify when this view is hidden
- */
- public void hide(@Nullable OnVisibilityChangedListener listener) {
- hide(listener, true);
- }
-
- void hide(@Nullable OnVisibilityChangedListener listener, boolean fromUser) {
- getImpl().hide(wrapOnVisibilityChangedListener(listener), fromUser);
- }
-
- /**
- * Set whether FloatingActionButton should add inner padding on platforms Lollipop and after,
- * to ensure consistent dimensions on all platforms.
- *
- * @param useCompatPadding true if FloatingActionButton is adding inner padding on platforms
- * Lollipop and after, to ensure consistent dimensions on all platforms.
- *
- * @attr ref android.support.design.R.styleable#FloatingActionButton_useCompatPadding
- * @see #getUseCompatPadding()
- */
- public void setUseCompatPadding(boolean useCompatPadding) {
- if (mCompatPadding != useCompatPadding) {
- mCompatPadding = useCompatPadding;
- getImpl().onCompatShadowChanged();
- }
- }
-
- /**
- * Returns whether FloatingActionButton will add inner padding on platforms Lollipop and after.
- *
- * @return true if FloatingActionButton is adding inner padding on platforms Lollipop and after,
- * to ensure consistent dimensions on all platforms.
- *
- * @attr ref android.support.design.R.styleable#FloatingActionButton_useCompatPadding
- * @see #setUseCompatPadding(boolean)
- */
- public boolean getUseCompatPadding() {
- return mCompatPadding;
- }
-
- /**
- * Sets the size of the button.
- *
- * <p>The options relate to the options available on the material design specification.
- * {@link #SIZE_NORMAL} is larger than {@link #SIZE_MINI}. {@link #SIZE_AUTO} will choose
- * an appropriate size based on the screen size.</p>
- *
- * @param size one of {@link #SIZE_NORMAL}, {@link #SIZE_MINI} or {@link #SIZE_AUTO}
- *
- * @attr ref android.support.design.R.styleable#FloatingActionButton_fabSize
- */
- public void setSize(@Size int size) {
- if (size != mSize) {
- mSize = size;
- requestLayout();
- }
- }
-
- /**
- * Returns the chosen size for this button.
- *
- * @return one of {@link #SIZE_NORMAL}, {@link #SIZE_MINI} or {@link #SIZE_AUTO}
- * @see #setSize(int)
- */
- @Size
- public int getSize() {
- return mSize;
- }
-
- @Nullable
- private InternalVisibilityChangedListener wrapOnVisibilityChangedListener(
- @Nullable final OnVisibilityChangedListener listener) {
- if (listener == null) {
- return null;
- }
-
- return new InternalVisibilityChangedListener() {
- @Override
- public void onShown() {
- listener.onShown(FloatingActionButton.this);
- }
-
- @Override
- public void onHidden() {
- listener.onHidden(FloatingActionButton.this);
- }
- };
- }
-
- /**
- * Sets the size of the button to be a custom value in pixels. If set to
- * {@link #NO_CUSTOM_SIZE}, custom size will not be used and size will be calculated according
- * to {@link #setSize(int)} method.
- *
- * @param size preferred size in pixels, or zero
- *
- * @attr ref android.support.design.R.styleable#FloatingActionButton_fabCustomSize
- */
- public void setCustomSize(int size) {
- if (size < 0) {
- throw new IllegalArgumentException("Custom size should be non-negative.");
- }
- mCustomSize = size;
- }
-
- /**
- * Returns the custom size for this button.
- *
- * @return size in pixels, or {@link #NO_CUSTOM_SIZE}
- */
- public int getCustomSize() {
- return mCustomSize;
- }
-
- int getSizeDimension() {
- return getSizeDimension(mSize);
- }
-
- private int getSizeDimension(@Size final int size) {
- final Resources res = getResources();
- // If custom size is set, return it
- if (mCustomSize != NO_CUSTOM_SIZE) {
- return mCustomSize;
- }
- switch (size) {
- case SIZE_AUTO:
- // If we're set to auto, grab the size from resources and refresh
- final int width = res.getConfiguration().screenWidthDp;
- final int height = res.getConfiguration().screenHeightDp;
- return Math.max(width, height) < AUTO_MINI_LARGEST_SCREEN_WIDTH
- ? getSizeDimension(SIZE_MINI)
- : getSizeDimension(SIZE_NORMAL);
- case SIZE_MINI:
- return res.getDimensionPixelSize(R.dimen.design_fab_size_mini);
- case SIZE_NORMAL:
- default:
- return res.getDimensionPixelSize(R.dimen.design_fab_size_normal);
- }
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- getImpl().onAttachedToWindow();
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- getImpl().onDetachedFromWindow();
- }
-
- @Override
- protected void drawableStateChanged() {
- super.drawableStateChanged();
- getImpl().onDrawableStateChanged(getDrawableState());
- }
-
- @Override
- public void jumpDrawablesToCurrentState() {
- super.jumpDrawablesToCurrentState();
- getImpl().jumpDrawableToCurrentState();
- }
-
- /**
- * Return in {@code rect} the bounds of the actual floating action button content in view-local
- * coordinates. This is defined as anything within any visible shadow.
- *
- * @return true if this view actually has been laid out and has a content rect, else false.
- */
- public boolean getContentRect(@NonNull Rect rect) {
- if (ViewCompat.isLaidOut(this)) {
- rect.set(0, 0, getWidth(), getHeight());
- rect.left += mShadowPadding.left;
- rect.top += mShadowPadding.top;
- rect.right -= mShadowPadding.right;
- rect.bottom -= mShadowPadding.bottom;
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Returns the FloatingActionButton's background, minus any compatible shadow implementation.
- */
- @NonNull
- public Drawable getContentBackground() {
- return getImpl().getContentBackground();
- }
-
- private static int resolveAdjustedSize(int desiredSize, int measureSpec) {
- int result = desiredSize;
- int specMode = MeasureSpec.getMode(measureSpec);
- int specSize = MeasureSpec.getSize(measureSpec);
- switch (specMode) {
- case MeasureSpec.UNSPECIFIED:
- // Parent says we can be as big as we want. Just don't be larger
- // than max size imposed on ourselves.
- result = desiredSize;
- break;
- case MeasureSpec.AT_MOST:
- // Parent says we can be as big as we want, up to specSize.
- // Don't be larger than specSize, and don't be larger than
- // the max size imposed on ourselves.
- result = Math.min(desiredSize, specSize);
- break;
- case MeasureSpec.EXACTLY:
- // No choice. Do what we are told.
- result = specSize;
- break;
- }
- return result;
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- switch (ev.getAction()) {
- case MotionEvent.ACTION_DOWN:
- // Skipping the gesture if it doesn't start in in the FAB 'content' area
- if (getContentRect(mTouchArea)
- && !mTouchArea.contains((int) ev.getX(), (int) ev.getY())) {
- return false;
- }
- break;
- }
- return super.onTouchEvent(ev);
- }
-
- /**
- * Behavior designed for use with {@link FloatingActionButton} instances. Its main function
- * is to move {@link FloatingActionButton} views so that any displayed {@link Snackbar}s do
- * not cover them.
- */
- public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
- private static final boolean AUTO_HIDE_DEFAULT = true;
-
- private Rect mTmpRect;
- private OnVisibilityChangedListener mInternalAutoHideListener;
- private boolean mAutoHideEnabled;
-
- public Behavior() {
- super();
- mAutoHideEnabled = AUTO_HIDE_DEFAULT;
- }
-
- public Behavior(Context context, AttributeSet attrs) {
- super(context, attrs);
- TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.FloatingActionButton_Behavior_Layout);
- mAutoHideEnabled = a.getBoolean(
- R.styleable.FloatingActionButton_Behavior_Layout_behavior_autoHide,
- AUTO_HIDE_DEFAULT);
- a.recycle();
- }
-
- /**
- * Sets whether the associated FloatingActionButton automatically hides when there is
- * not enough space to be displayed. This works with {@link AppBarLayout}
- * and {@link BottomSheetBehavior}.
- *
- * @attr ref android.support.design.R.styleable#FloatingActionButton_Behavior_Layout_behavior_autoHide
- * @param autoHide true to enable automatic hiding
- */
- public void setAutoHideEnabled(boolean autoHide) {
- mAutoHideEnabled = autoHide;
- }
-
- /**
- * Returns whether the associated FloatingActionButton automatically hides when there is
- * not enough space to be displayed.
- *
- * @attr ref android.support.design.R.styleable#FloatingActionButton_Behavior_Layout_behavior_autoHide
- * @return true if enabled
- */
- public boolean isAutoHideEnabled() {
- return mAutoHideEnabled;
- }
-
- @Override
- public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams lp) {
- if (lp.dodgeInsetEdges == Gravity.NO_GRAVITY) {
- // If the developer hasn't set dodgeInsetEdges, lets set it to BOTTOM so that
- // we dodge any Snackbars
- lp.dodgeInsetEdges = Gravity.BOTTOM;
- }
- }
-
- @Override
- public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
- View dependency) {
- if (dependency instanceof AppBarLayout) {
- // If we're depending on an AppBarLayout we will show/hide it automatically
- // if the FAB is anchored to the AppBarLayout
- updateFabVisibilityForAppBarLayout(parent, (AppBarLayout) dependency, child);
- } else if (isBottomSheet(dependency)) {
- updateFabVisibilityForBottomSheet(dependency, child);
- }
- return false;
- }
-
- private static boolean isBottomSheet(@NonNull View view) {
- final ViewGroup.LayoutParams lp = view.getLayoutParams();
- if (lp instanceof CoordinatorLayout.LayoutParams) {
- return ((CoordinatorLayout.LayoutParams) lp)
- .getBehavior() instanceof BottomSheetBehavior;
- }
- return false;
- }
-
- @VisibleForTesting
- void setInternalAutoHideListener(OnVisibilityChangedListener listener) {
- mInternalAutoHideListener = listener;
- }
-
- private boolean shouldUpdateVisibility(View dependency, FloatingActionButton child) {
- final CoordinatorLayout.LayoutParams lp =
- (CoordinatorLayout.LayoutParams) child.getLayoutParams();
- if (!mAutoHideEnabled) {
- return false;
- }
-
- if (lp.getAnchorId() != dependency.getId()) {
- // The anchor ID doesn't match the dependency, so we won't automatically
- // show/hide the FAB
- return false;
- }
-
- //noinspection RedundantIfStatement
- if (child.getUserSetVisibility() != VISIBLE) {
- // The view isn't set to be visible so skip changing its visibility
- return false;
- }
-
- return true;
- }
-
- private boolean updateFabVisibilityForAppBarLayout(CoordinatorLayout parent,
- AppBarLayout appBarLayout, FloatingActionButton child) {
- if (!shouldUpdateVisibility(appBarLayout, child)) {
- return false;
- }
-
- if (mTmpRect == null) {
- mTmpRect = new Rect();
- }
-
- // First, let's get the visible rect of the dependency
- final Rect rect = mTmpRect;
- ViewGroupUtils.getDescendantRect(parent, appBarLayout, rect);
-
- if (rect.bottom <= appBarLayout.getMinimumHeightForVisibleOverlappingContent()) {
- // If the anchor's bottom is below the seam, we'll animate our FAB out
- child.hide(mInternalAutoHideListener, false);
- } else {
- // Else, we'll animate our FAB back in
- child.show(mInternalAutoHideListener, false);
- }
- return true;
- }
-
- private boolean updateFabVisibilityForBottomSheet(View bottomSheet,
- FloatingActionButton child) {
- if (!shouldUpdateVisibility(bottomSheet, child)) {
- return false;
- }
- CoordinatorLayout.LayoutParams lp =
- (CoordinatorLayout.LayoutParams) child.getLayoutParams();
- if (bottomSheet.getTop() < child.getHeight() / 2 + lp.topMargin) {
- child.hide(mInternalAutoHideListener, false);
- } else {
- child.show(mInternalAutoHideListener, false);
- }
- return true;
- }
-
- @Override
- public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child,
- int layoutDirection) {
- // First, let's make sure that the visibility of the FAB is consistent
- final List<View> dependencies = parent.getDependencies(child);
- for (int i = 0, count = dependencies.size(); i < count; i++) {
- final View dependency = dependencies.get(i);
- if (dependency instanceof AppBarLayout) {
- if (updateFabVisibilityForAppBarLayout(
- parent, (AppBarLayout) dependency, child)) {
- break;
- }
- } else if (isBottomSheet(dependency)) {
- if (updateFabVisibilityForBottomSheet(dependency, child)) {
- break;
- }
- }
- }
- // Now let the CoordinatorLayout lay out the FAB
- parent.onLayoutChild(child, layoutDirection);
- // Now offset it if needed
- offsetIfNeeded(parent, child);
- return true;
- }
-
- @Override
- public boolean getInsetDodgeRect(@NonNull CoordinatorLayout parent,
- @NonNull FloatingActionButton child, @NonNull Rect rect) {
- // Since we offset so that any internal shadow padding isn't shown, we need to make
- // sure that the shadow isn't used for any dodge inset calculations
- final Rect shadowPadding = child.mShadowPadding;
- rect.set(child.getLeft() + shadowPadding.left,
- child.getTop() + shadowPadding.top,
- child.getRight() - shadowPadding.right,
- child.getBottom() - shadowPadding.bottom);
- return true;
- }
-
- /**
- * Pre-Lollipop we use padding so that the shadow has enough space to be drawn. This method
- * offsets our layout position so that we're positioned correctly if we're on one of
- * our parent's edges.
- */
- private void offsetIfNeeded(CoordinatorLayout parent, FloatingActionButton fab) {
- final Rect padding = fab.mShadowPadding;
-
- if (padding != null && padding.centerX() > 0 && padding.centerY() > 0) {
- final CoordinatorLayout.LayoutParams lp =
- (CoordinatorLayout.LayoutParams) fab.getLayoutParams();
-
- int offsetTB = 0, offsetLR = 0;
-
- if (fab.getRight() >= parent.getWidth() - lp.rightMargin) {
- // If we're on the right edge, shift it the right
- offsetLR = padding.right;
- } else if (fab.getLeft() <= lp.leftMargin) {
- // If we're on the left edge, shift it the left
- offsetLR = -padding.left;
- }
- if (fab.getBottom() >= parent.getHeight() - lp.bottomMargin) {
- // If we're on the bottom edge, shift it down
- offsetTB = padding.bottom;
- } else if (fab.getTop() <= lp.topMargin) {
- // If we're on the top edge, shift it up
- offsetTB = -padding.top;
- }
-
- if (offsetTB != 0) {
- ViewCompat.offsetTopAndBottom(fab, offsetTB);
- }
- if (offsetLR != 0) {
- ViewCompat.offsetLeftAndRight(fab, offsetLR);
- }
- }
- }
- }
-
- /**
- * Returns the backward compatible elevation of the FloatingActionButton.
- *
- * @return the backward compatible elevation in pixels.
- * @attr ref android.support.design.R.styleable#FloatingActionButton_elevation
- * @see #setCompatElevation(float)
- */
- public float getCompatElevation() {
- return getImpl().getElevation();
- }
-
- /**
- * Updates the backward compatible elevation of the FloatingActionButton.
- *
- * @param elevation The backward compatible elevation in pixels.
- * @attr ref android.support.design.R.styleable#FloatingActionButton_elevation
- * @see #getCompatElevation()
- * @see #setUseCompatPadding(boolean)
- */
- public void setCompatElevation(float elevation) {
- getImpl().setElevation(elevation);
- }
-
- private FloatingActionButtonImpl getImpl() {
- if (mImpl == null) {
- mImpl = createImpl();
- }
- return mImpl;
- }
-
- private FloatingActionButtonImpl createImpl() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- return new FloatingActionButtonLollipop(this, new ShadowDelegateImpl());
- } else {
- return new FloatingActionButtonImpl(this, new ShadowDelegateImpl());
- }
- }
-
- private class ShadowDelegateImpl implements ShadowViewDelegate {
- ShadowDelegateImpl() {
- }
-
- @Override
- public float getRadius() {
- return getSizeDimension() / 2f;
- }
-
- @Override
- public void setShadowPadding(int left, int top, int right, int bottom) {
- mShadowPadding.set(left, top, right, bottom);
- setPadding(left + mImagePadding, top + mImagePadding,
- right + mImagePadding, bottom + mImagePadding);
- }
-
- @Override
- public void setBackgroundDrawable(Drawable background) {
- FloatingActionButton.super.setBackgroundDrawable(background);
- }
-
- @Override
- public boolean isCompatPaddingEnabled() {
- return mCompatPadding;
- }
- }
-}
diff --git a/android/support/design/widget/FloatingActionButtonImpl.java b/android/support/design/widget/FloatingActionButtonImpl.java
deleted file mode 100644
index 132cd81..0000000
--- a/android/support/design/widget/FloatingActionButtonImpl.java
+++ /dev/null
@@ -1,531 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.LayerDrawable;
-import android.os.Build;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RequiresApi;
-import android.support.design.R;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import android.support.v4.view.ViewCompat;
-import android.view.View;
-import android.view.ViewTreeObserver;
-import android.view.animation.Interpolator;
-
-@RequiresApi(14)
-class FloatingActionButtonImpl {
- static final Interpolator ANIM_INTERPOLATOR = AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR;
- static final long PRESSED_ANIM_DURATION = 100;
- static final long PRESSED_ANIM_DELAY = 100;
-
- static final int ANIM_STATE_NONE = 0;
- static final int ANIM_STATE_HIDING = 1;
- static final int ANIM_STATE_SHOWING = 2;
-
- int mAnimState = ANIM_STATE_NONE;
-
- private final StateListAnimator mStateListAnimator;
-
- ShadowDrawableWrapper mShadowDrawable;
-
- private float mRotation;
-
- Drawable mShapeDrawable;
- Drawable mRippleDrawable;
- CircularBorderDrawable mBorderDrawable;
- Drawable mContentBackground;
-
- float mElevation;
- float mPressedTranslationZ;
-
- interface InternalVisibilityChangedListener {
- void onShown();
- void onHidden();
- }
-
- static final int SHOW_HIDE_ANIM_DURATION = 200;
-
- static final int[] PRESSED_ENABLED_STATE_SET = {android.R.attr.state_pressed,
- android.R.attr.state_enabled};
- static final int[] FOCUSED_ENABLED_STATE_SET = {android.R.attr.state_focused,
- android.R.attr.state_enabled};
- static final int[] ENABLED_STATE_SET = {android.R.attr.state_enabled};
- static final int[] EMPTY_STATE_SET = new int[0];
-
- final VisibilityAwareImageButton mView;
- final ShadowViewDelegate mShadowViewDelegate;
-
- private final Rect mTmpRect = new Rect();
- private ViewTreeObserver.OnPreDrawListener mPreDrawListener;
-
- FloatingActionButtonImpl(VisibilityAwareImageButton view,
- ShadowViewDelegate shadowViewDelegate) {
- mView = view;
- mShadowViewDelegate = shadowViewDelegate;
-
- mStateListAnimator = new StateListAnimator();
-
- // Elevate with translationZ when pressed or focused
- mStateListAnimator.addState(PRESSED_ENABLED_STATE_SET,
- createAnimator(new ElevateToTranslationZAnimation()));
- mStateListAnimator.addState(FOCUSED_ENABLED_STATE_SET,
- createAnimator(new ElevateToTranslationZAnimation()));
- // Reset back to elevation by default
- mStateListAnimator.addState(ENABLED_STATE_SET,
- createAnimator(new ResetElevationAnimation()));
- // Set to 0 when disabled
- mStateListAnimator.addState(EMPTY_STATE_SET,
- createAnimator(new DisabledElevationAnimation()));
-
- mRotation = mView.getRotation();
- }
-
- void setBackgroundDrawable(ColorStateList backgroundTint,
- PorterDuff.Mode backgroundTintMode, int rippleColor, int borderWidth) {
- // Now we need to tint the original background with the tint, using
- // an InsetDrawable if we have a border width
- mShapeDrawable = DrawableCompat.wrap(createShapeDrawable());
- DrawableCompat.setTintList(mShapeDrawable, backgroundTint);
- if (backgroundTintMode != null) {
- DrawableCompat.setTintMode(mShapeDrawable, backgroundTintMode);
- }
-
- // Now we created a mask Drawable which will be used for touch feedback.
- GradientDrawable touchFeedbackShape = createShapeDrawable();
-
- // We'll now wrap that touch feedback mask drawable with a ColorStateList. We do not need
- // to inset for any border here as LayerDrawable will nest the padding for us
- mRippleDrawable = DrawableCompat.wrap(touchFeedbackShape);
- DrawableCompat.setTintList(mRippleDrawable, createColorStateList(rippleColor));
-
- final Drawable[] layers;
- if (borderWidth > 0) {
- mBorderDrawable = createBorderDrawable(borderWidth, backgroundTint);
- layers = new Drawable[] {mBorderDrawable, mShapeDrawable, mRippleDrawable};
- } else {
- mBorderDrawable = null;
- layers = new Drawable[] {mShapeDrawable, mRippleDrawable};
- }
-
- mContentBackground = new LayerDrawable(layers);
-
- mShadowDrawable = new ShadowDrawableWrapper(
- mView.getContext(),
- mContentBackground,
- mShadowViewDelegate.getRadius(),
- mElevation,
- mElevation + mPressedTranslationZ);
- mShadowDrawable.setAddPaddingForCorners(false);
- mShadowViewDelegate.setBackgroundDrawable(mShadowDrawable);
- }
-
- void setBackgroundTintList(ColorStateList tint) {
- if (mShapeDrawable != null) {
- DrawableCompat.setTintList(mShapeDrawable, tint);
- }
- if (mBorderDrawable != null) {
- mBorderDrawable.setBorderTint(tint);
- }
- }
-
- void setBackgroundTintMode(PorterDuff.Mode tintMode) {
- if (mShapeDrawable != null) {
- DrawableCompat.setTintMode(mShapeDrawable, tintMode);
- }
- }
-
-
- void setRippleColor(int rippleColor) {
- if (mRippleDrawable != null) {
- DrawableCompat.setTintList(mRippleDrawable, createColorStateList(rippleColor));
- }
- }
-
- final void setElevation(float elevation) {
- if (mElevation != elevation) {
- mElevation = elevation;
- onElevationsChanged(elevation, mPressedTranslationZ);
- }
- }
-
- float getElevation() {
- return mElevation;
- }
-
- final void setPressedTranslationZ(float translationZ) {
- if (mPressedTranslationZ != translationZ) {
- mPressedTranslationZ = translationZ;
- onElevationsChanged(mElevation, translationZ);
- }
- }
-
- void onElevationsChanged(float elevation, float pressedTranslationZ) {
- if (mShadowDrawable != null) {
- mShadowDrawable.setShadowSize(elevation, elevation + mPressedTranslationZ);
- updatePadding();
- }
- }
-
- void onDrawableStateChanged(int[] state) {
- mStateListAnimator.setState(state);
- }
-
- void jumpDrawableToCurrentState() {
- mStateListAnimator.jumpToCurrentState();
- }
-
- void hide(@Nullable final InternalVisibilityChangedListener listener, final boolean fromUser) {
- if (isOrWillBeHidden()) {
- // We either are or will soon be hidden, skip the call
- return;
- }
-
- mView.animate().cancel();
-
- if (shouldAnimateVisibilityChange()) {
- mAnimState = ANIM_STATE_HIDING;
-
- mView.animate()
- .scaleX(0f)
- .scaleY(0f)
- .alpha(0f)
- .setDuration(SHOW_HIDE_ANIM_DURATION)
- .setInterpolator(AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR)
- .setListener(new AnimatorListenerAdapter() {
- private boolean mCancelled;
-
- @Override
- public void onAnimationStart(Animator animation) {
- mView.internalSetVisibility(View.VISIBLE, fromUser);
- mCancelled = false;
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- mCancelled = true;
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- mAnimState = ANIM_STATE_NONE;
-
- if (!mCancelled) {
- mView.internalSetVisibility(fromUser ? View.GONE : View.INVISIBLE,
- fromUser);
- if (listener != null) {
- listener.onHidden();
- }
- }
- }
- });
- } else {
- // If the view isn't laid out, or we're in the editor, don't run the animation
- mView.internalSetVisibility(fromUser ? View.GONE : View.INVISIBLE, fromUser);
- if (listener != null) {
- listener.onHidden();
- }
- }
- }
-
- void show(@Nullable final InternalVisibilityChangedListener listener, final boolean fromUser) {
- if (isOrWillBeShown()) {
- // We either are or will soon be visible, skip the call
- return;
- }
-
- mView.animate().cancel();
-
- if (shouldAnimateVisibilityChange()) {
- mAnimState = ANIM_STATE_SHOWING;
-
- if (mView.getVisibility() != View.VISIBLE) {
- // If the view isn't visible currently, we'll animate it from a single pixel
- mView.setAlpha(0f);
- mView.setScaleY(0f);
- mView.setScaleX(0f);
- }
-
- mView.animate()
- .scaleX(1f)
- .scaleY(1f)
- .alpha(1f)
- .setDuration(SHOW_HIDE_ANIM_DURATION)
- .setInterpolator(AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR)
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- mView.internalSetVisibility(View.VISIBLE, fromUser);
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- mAnimState = ANIM_STATE_NONE;
- if (listener != null) {
- listener.onShown();
- }
- }
- });
- } else {
- mView.internalSetVisibility(View.VISIBLE, fromUser);
- mView.setAlpha(1f);
- mView.setScaleY(1f);
- mView.setScaleX(1f);
- if (listener != null) {
- listener.onShown();
- }
- }
- }
-
- final Drawable getContentBackground() {
- return mContentBackground;
- }
-
- void onCompatShadowChanged() {
- // Ignore pre-v21
- }
-
- final void updatePadding() {
- Rect rect = mTmpRect;
- getPadding(rect);
- onPaddingUpdated(rect);
- mShadowViewDelegate.setShadowPadding(rect.left, rect.top, rect.right, rect.bottom);
- }
-
- void getPadding(Rect rect) {
- mShadowDrawable.getPadding(rect);
- }
-
- void onPaddingUpdated(Rect padding) {}
-
- void onAttachedToWindow() {
- if (requirePreDrawListener()) {
- ensurePreDrawListener();
- mView.getViewTreeObserver().addOnPreDrawListener(mPreDrawListener);
- }
- }
-
- void onDetachedFromWindow() {
- if (mPreDrawListener != null) {
- mView.getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener);
- mPreDrawListener = null;
- }
- }
-
- boolean requirePreDrawListener() {
- return true;
- }
-
- CircularBorderDrawable createBorderDrawable(int borderWidth, ColorStateList backgroundTint) {
- final Context context = mView.getContext();
- CircularBorderDrawable borderDrawable = newCircularDrawable();
- borderDrawable.setGradientColors(
- ContextCompat.getColor(context, R.color.design_fab_stroke_top_outer_color),
- ContextCompat.getColor(context, R.color.design_fab_stroke_top_inner_color),
- ContextCompat.getColor(context, R.color.design_fab_stroke_end_inner_color),
- ContextCompat.getColor(context, R.color.design_fab_stroke_end_outer_color));
- borderDrawable.setBorderWidth(borderWidth);
- borderDrawable.setBorderTint(backgroundTint);
- return borderDrawable;
- }
-
- CircularBorderDrawable newCircularDrawable() {
- return new CircularBorderDrawable();
- }
-
- void onPreDraw() {
- final float rotation = mView.getRotation();
- if (mRotation != rotation) {
- mRotation = rotation;
- updateFromViewRotation();
- }
- }
-
- private void ensurePreDrawListener() {
- if (mPreDrawListener == null) {
- mPreDrawListener = new ViewTreeObserver.OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- FloatingActionButtonImpl.this.onPreDraw();
- return true;
- }
- };
- }
- }
-
- GradientDrawable createShapeDrawable() {
- GradientDrawable d = newGradientDrawableForShape();
- d.setShape(GradientDrawable.OVAL);
- d.setColor(Color.WHITE);
- return d;
- }
-
- GradientDrawable newGradientDrawableForShape() {
- return new GradientDrawable();
- }
-
- boolean isOrWillBeShown() {
- if (mView.getVisibility() != View.VISIBLE) {
- // If we not currently visible, return true if we're animating to be shown
- return mAnimState == ANIM_STATE_SHOWING;
- } else {
- // Otherwise if we're visible, return true if we're not animating to be hidden
- return mAnimState != ANIM_STATE_HIDING;
- }
- }
-
- boolean isOrWillBeHidden() {
- if (mView.getVisibility() == View.VISIBLE) {
- // If we currently visible, return true if we're animating to be hidden
- return mAnimState == ANIM_STATE_HIDING;
- } else {
- // Otherwise if we're not visible, return true if we're not animating to be shown
- return mAnimState != ANIM_STATE_SHOWING;
- }
- }
-
- private ValueAnimator createAnimator(@NonNull ShadowAnimatorImpl impl) {
- final ValueAnimator animator = new ValueAnimator();
- animator.setInterpolator(ANIM_INTERPOLATOR);
- animator.setDuration(PRESSED_ANIM_DURATION);
- animator.addListener(impl);
- animator.addUpdateListener(impl);
- animator.setFloatValues(0, 1);
- return animator;
- }
-
- private abstract class ShadowAnimatorImpl extends AnimatorListenerAdapter
- implements ValueAnimator.AnimatorUpdateListener {
- private boolean mValidValues;
- private float mShadowSizeStart;
- private float mShadowSizeEnd;
-
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- if (!mValidValues) {
- mShadowSizeStart = mShadowDrawable.getShadowSize();
- mShadowSizeEnd = getTargetShadowSize();
- mValidValues = true;
- }
-
- mShadowDrawable.setShadowSize(mShadowSizeStart
- + ((mShadowSizeEnd - mShadowSizeStart) * animator.getAnimatedFraction()));
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- mShadowDrawable.setShadowSize(mShadowSizeEnd);
- mValidValues = false;
- }
-
- /**
- * @return the shadow size we want to animate to.
- */
- protected abstract float getTargetShadowSize();
- }
-
- private class ResetElevationAnimation extends ShadowAnimatorImpl {
- ResetElevationAnimation() {
- }
-
- @Override
- protected float getTargetShadowSize() {
- return mElevation;
- }
- }
-
- private class ElevateToTranslationZAnimation extends ShadowAnimatorImpl {
- ElevateToTranslationZAnimation() {
- }
-
- @Override
- protected float getTargetShadowSize() {
- return mElevation + mPressedTranslationZ;
- }
- }
-
- private class DisabledElevationAnimation extends ShadowAnimatorImpl {
- DisabledElevationAnimation() {
- }
-
- @Override
- protected float getTargetShadowSize() {
- return 0f;
- }
- }
-
- private static ColorStateList createColorStateList(int selectedColor) {
- final int[][] states = new int[3][];
- final int[] colors = new int[3];
- int i = 0;
-
- states[i] = FOCUSED_ENABLED_STATE_SET;
- colors[i] = selectedColor;
- i++;
-
- states[i] = PRESSED_ENABLED_STATE_SET;
- colors[i] = selectedColor;
- i++;
-
- // Default enabled state
- states[i] = new int[0];
- colors[i] = Color.TRANSPARENT;
- i++;
-
- return new ColorStateList(states, colors);
- }
-
- private boolean shouldAnimateVisibilityChange() {
- return ViewCompat.isLaidOut(mView) && !mView.isInEditMode();
- }
-
- private void updateFromViewRotation() {
- if (Build.VERSION.SDK_INT == 19) {
- // KitKat seems to have an issue with views which are rotated with angles which are
- // not divisible by 90. Worked around by moving to software rendering in these cases.
- if ((mRotation % 90) != 0) {
- if (mView.getLayerType() != View.LAYER_TYPE_SOFTWARE) {
- mView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
- }
- } else {
- if (mView.getLayerType() != View.LAYER_TYPE_NONE) {
- mView.setLayerType(View.LAYER_TYPE_NONE, null);
- }
- }
- }
-
- // Offset any View rotation
- if (mShadowDrawable != null) {
- mShadowDrawable.setRotation(-mRotation);
- }
- if (mBorderDrawable != null) {
- mBorderDrawable.setRotation(-mRotation);
- }
- }
-}
diff --git a/android/support/design/widget/FloatingActionButtonLollipop.java b/android/support/design/widget/FloatingActionButtonLollipop.java
deleted file mode 100644
index 0df83da..0000000
--- a/android/support/design/widget/FloatingActionButtonLollipop.java
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.StateListAnimator;
-import android.content.res.ColorStateList;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.InsetDrawable;
-import android.graphics.drawable.LayerDrawable;
-import android.graphics.drawable.RippleDrawable;
-import android.os.Build;
-import android.support.annotation.RequiresApi;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import android.view.View;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@RequiresApi(21)
-class FloatingActionButtonLollipop extends FloatingActionButtonImpl {
-
- private InsetDrawable mInsetDrawable;
-
- FloatingActionButtonLollipop(VisibilityAwareImageButton view,
- ShadowViewDelegate shadowViewDelegate) {
- super(view, shadowViewDelegate);
- }
-
- @Override
- void setBackgroundDrawable(ColorStateList backgroundTint,
- PorterDuff.Mode backgroundTintMode, int rippleColor, int borderWidth) {
- // Now we need to tint the shape background with the tint
- mShapeDrawable = DrawableCompat.wrap(createShapeDrawable());
- DrawableCompat.setTintList(mShapeDrawable, backgroundTint);
- if (backgroundTintMode != null) {
- DrawableCompat.setTintMode(mShapeDrawable, backgroundTintMode);
- }
-
- final Drawable rippleContent;
- if (borderWidth > 0) {
- mBorderDrawable = createBorderDrawable(borderWidth, backgroundTint);
- rippleContent = new LayerDrawable(new Drawable[]{mBorderDrawable, mShapeDrawable});
- } else {
- mBorderDrawable = null;
- rippleContent = mShapeDrawable;
- }
-
- mRippleDrawable = new RippleDrawable(ColorStateList.valueOf(rippleColor),
- rippleContent, null);
-
- mContentBackground = mRippleDrawable;
-
- mShadowViewDelegate.setBackgroundDrawable(mRippleDrawable);
- }
-
- @Override
- void setRippleColor(int rippleColor) {
- if (mRippleDrawable instanceof RippleDrawable) {
- ((RippleDrawable) mRippleDrawable).setColor(ColorStateList.valueOf(rippleColor));
- } else {
- super.setRippleColor(rippleColor);
- }
- }
-
- @Override
- void onElevationsChanged(final float elevation, final float pressedTranslationZ) {
- if (Build.VERSION.SDK_INT == 21) {
- // Animations produce NPE in version 21. Bluntly set the values instead (matching the
- // logic in the animations below).
- if (mView.isEnabled()) {
- mView.setElevation(elevation);
- if (mView.isFocused() || mView.isPressed()) {
- mView.setTranslationZ(pressedTranslationZ);
- } else {
- mView.setTranslationZ(0);
- }
- } else {
- mView.setElevation(0);
- mView.setTranslationZ(0);
- }
- } else {
- final StateListAnimator stateListAnimator = new StateListAnimator();
-
- // Animate elevation and translationZ to our values when pressed
- AnimatorSet set = new AnimatorSet();
- set.play(ObjectAnimator.ofFloat(mView, "elevation", elevation).setDuration(0))
- .with(ObjectAnimator.ofFloat(mView, View.TRANSLATION_Z, pressedTranslationZ)
- .setDuration(PRESSED_ANIM_DURATION));
- set.setInterpolator(ANIM_INTERPOLATOR);
- stateListAnimator.addState(PRESSED_ENABLED_STATE_SET, set);
-
- // Same deal for when we're focused
- set = new AnimatorSet();
- set.play(ObjectAnimator.ofFloat(mView, "elevation", elevation).setDuration(0))
- .with(ObjectAnimator.ofFloat(mView, View.TRANSLATION_Z, pressedTranslationZ)
- .setDuration(PRESSED_ANIM_DURATION));
- set.setInterpolator(ANIM_INTERPOLATOR);
- stateListAnimator.addState(FOCUSED_ENABLED_STATE_SET, set);
-
- // Animate translationZ to 0 if not pressed
- set = new AnimatorSet();
- List<Animator> animators = new ArrayList<>();
- animators.add(ObjectAnimator.ofFloat(mView, "elevation", elevation).setDuration(0));
- if (Build.VERSION.SDK_INT >= 22 && Build.VERSION.SDK_INT <= 24) {
- // This is a no-op animation which exists here only for introducing the duration
- // because setting the delay (on the next animation) via "setDelay" or "after"
- // can trigger a NPE between android versions 22 and 24 (due to a framework
- // bug). The issue has been fixed in version 25.
- animators.add(ObjectAnimator.ofFloat(mView, View.TRANSLATION_Z,
- mView.getTranslationZ()).setDuration(PRESSED_ANIM_DELAY));
- }
- animators.add(ObjectAnimator.ofFloat(mView, View.TRANSLATION_Z, 0f)
- .setDuration(PRESSED_ANIM_DURATION));
- set.playSequentially(animators.toArray(new ObjectAnimator[0]));
- set.setInterpolator(ANIM_INTERPOLATOR);
- stateListAnimator.addState(ENABLED_STATE_SET, set);
-
- // Animate everything to 0 when disabled
- set = new AnimatorSet();
- set.play(ObjectAnimator.ofFloat(mView, "elevation", 0f).setDuration(0))
- .with(ObjectAnimator.ofFloat(mView, View.TRANSLATION_Z, 0f).setDuration(0));
- set.setInterpolator(ANIM_INTERPOLATOR);
- stateListAnimator.addState(EMPTY_STATE_SET, set);
-
- mView.setStateListAnimator(stateListAnimator);
- }
-
- if (mShadowViewDelegate.isCompatPaddingEnabled()) {
- updatePadding();
- }
- }
-
- @Override
- public float getElevation() {
- return mView.getElevation();
- }
-
- @Override
- void onCompatShadowChanged() {
- updatePadding();
- }
-
- @Override
- void onPaddingUpdated(Rect padding) {
- if (mShadowViewDelegate.isCompatPaddingEnabled()) {
- mInsetDrawable = new InsetDrawable(mRippleDrawable,
- padding.left, padding.top, padding.right, padding.bottom);
- mShadowViewDelegate.setBackgroundDrawable(mInsetDrawable);
- } else {
- mShadowViewDelegate.setBackgroundDrawable(mRippleDrawable);
- }
- }
-
- @Override
- void onDrawableStateChanged(int[] state) {
- // no-op
- }
-
- @Override
- void jumpDrawableToCurrentState() {
- // no-op
- }
-
- @Override
- boolean requirePreDrawListener() {
- return false;
- }
-
- @Override
- CircularBorderDrawable newCircularDrawable() {
- return new CircularBorderDrawableLollipop();
- }
-
- @Override
- GradientDrawable newGradientDrawableForShape() {
- return new AlwaysStatefulGradientDrawable();
- }
-
- @Override
- void getPadding(Rect rect) {
- if (mShadowViewDelegate.isCompatPaddingEnabled()) {
- final float radius = mShadowViewDelegate.getRadius();
- final float maxShadowSize = getElevation() + mPressedTranslationZ;
- final int hPadding = (int) Math.ceil(
- ShadowDrawableWrapper.calculateHorizontalPadding(maxShadowSize, radius, false));
- final int vPadding = (int) Math.ceil(
- ShadowDrawableWrapper.calculateVerticalPadding(maxShadowSize, radius, false));
- rect.set(hPadding, vPadding, hPadding, vPadding);
- } else {
- rect.set(0, 0, 0, 0);
- }
- }
-
- /**
- * LayerDrawable on L+ caches its isStateful() state and doesn't refresh it,
- * meaning that if we apply a tint to one of its children, the parent doesn't become
- * stateful and the tint doesn't work for state changes. We workaround it by saying that we
- * are always stateful. If we don't have a stateful tint, the change is ignored anyway.
- */
- static class AlwaysStatefulGradientDrawable extends GradientDrawable {
- @Override
- public boolean isStateful() {
- return true;
- }
- }
-}
diff --git a/android/support/design/widget/HeaderBehavior.java b/android/support/design/widget/HeaderBehavior.java
deleted file mode 100644
index a5d0edf..0000000
--- a/android/support/design/widget/HeaderBehavior.java
+++ /dev/null
@@ -1,307 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.content.Context;
-import android.support.design.widget.CoordinatorLayout.Behavior;
-import android.support.v4.math.MathUtils;
-import android.support.v4.view.ViewCompat;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.widget.OverScroller;
-
-/**
- * The {@link Behavior} for a view that sits vertically above scrolling a view.
- * See {@link HeaderScrollingViewBehavior}.
- */
-abstract class HeaderBehavior<V extends View> extends ViewOffsetBehavior<V> {
-
- private static final int INVALID_POINTER = -1;
-
- private Runnable mFlingRunnable;
- OverScroller mScroller;
-
- private boolean mIsBeingDragged;
- private int mActivePointerId = INVALID_POINTER;
- private int mLastMotionY;
- private int mTouchSlop = -1;
- private VelocityTracker mVelocityTracker;
-
- public HeaderBehavior() {}
-
- public HeaderBehavior(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
- if (mTouchSlop < 0) {
- mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
- }
-
- final int action = ev.getAction();
-
- // Shortcut since we're being dragged
- if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {
- return true;
- }
-
- switch (ev.getActionMasked()) {
- case MotionEvent.ACTION_DOWN: {
- mIsBeingDragged = false;
- final int x = (int) ev.getX();
- final int y = (int) ev.getY();
- if (canDragView(child) && parent.isPointInChildBounds(child, x, y)) {
- mLastMotionY = y;
- mActivePointerId = ev.getPointerId(0);
- ensureVelocityTracker();
- }
- break;
- }
-
- case MotionEvent.ACTION_MOVE: {
- final int activePointerId = mActivePointerId;
- if (activePointerId == INVALID_POINTER) {
- // If we don't have a valid id, the touch down wasn't on content.
- break;
- }
- final int pointerIndex = ev.findPointerIndex(activePointerId);
- if (pointerIndex == -1) {
- break;
- }
-
- final int y = (int) ev.getY(pointerIndex);
- final int yDiff = Math.abs(y - mLastMotionY);
- if (yDiff > mTouchSlop) {
- mIsBeingDragged = true;
- mLastMotionY = y;
- }
- break;
- }
-
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP: {
- mIsBeingDragged = false;
- mActivePointerId = INVALID_POINTER;
- if (mVelocityTracker != null) {
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
- break;
- }
- }
-
- if (mVelocityTracker != null) {
- mVelocityTracker.addMovement(ev);
- }
-
- return mIsBeingDragged;
- }
-
- @Override
- public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
- if (mTouchSlop < 0) {
- mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
- }
-
- switch (ev.getActionMasked()) {
- case MotionEvent.ACTION_DOWN: {
- final int x = (int) ev.getX();
- final int y = (int) ev.getY();
-
- if (parent.isPointInChildBounds(child, x, y) && canDragView(child)) {
- mLastMotionY = y;
- mActivePointerId = ev.getPointerId(0);
- ensureVelocityTracker();
- } else {
- return false;
- }
- break;
- }
-
- case MotionEvent.ACTION_MOVE: {
- final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
- if (activePointerIndex == -1) {
- return false;
- }
-
- final int y = (int) ev.getY(activePointerIndex);
- int dy = mLastMotionY - y;
-
- if (!mIsBeingDragged && Math.abs(dy) > mTouchSlop) {
- mIsBeingDragged = true;
- if (dy > 0) {
- dy -= mTouchSlop;
- } else {
- dy += mTouchSlop;
- }
- }
-
- if (mIsBeingDragged) {
- mLastMotionY = y;
- // We're being dragged so scroll the ABL
- scroll(parent, child, dy, getMaxDragOffset(child), 0);
- }
- break;
- }
-
- case MotionEvent.ACTION_UP:
- if (mVelocityTracker != null) {
- mVelocityTracker.addMovement(ev);
- mVelocityTracker.computeCurrentVelocity(1000);
- float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
- fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
- }
- // $FALLTHROUGH
- case MotionEvent.ACTION_CANCEL: {
- mIsBeingDragged = false;
- mActivePointerId = INVALID_POINTER;
- if (mVelocityTracker != null) {
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
- break;
- }
- }
-
- if (mVelocityTracker != null) {
- mVelocityTracker.addMovement(ev);
- }
-
- return true;
- }
-
- int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset) {
- return setHeaderTopBottomOffset(parent, header, newOffset,
- Integer.MIN_VALUE, Integer.MAX_VALUE);
- }
-
- int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset,
- int minOffset, int maxOffset) {
- final int curOffset = getTopAndBottomOffset();
- int consumed = 0;
-
- if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
- // If we have some scrolling range, and we're currently within the min and max
- // offsets, calculate a new offset
- newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
-
- if (curOffset != newOffset) {
- setTopAndBottomOffset(newOffset);
- // Update how much dy we have consumed
- consumed = curOffset - newOffset;
- }
- }
-
- return consumed;
- }
-
- int getTopBottomOffsetForScrollingSibling() {
- return getTopAndBottomOffset();
- }
-
- final int scroll(CoordinatorLayout coordinatorLayout, V header,
- int dy, int minOffset, int maxOffset) {
- return setHeaderTopBottomOffset(coordinatorLayout, header,
- getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);
- }
-
- final boolean fling(CoordinatorLayout coordinatorLayout, V layout, int minOffset,
- int maxOffset, float velocityY) {
- if (mFlingRunnable != null) {
- layout.removeCallbacks(mFlingRunnable);
- mFlingRunnable = null;
- }
-
- if (mScroller == null) {
- mScroller = new OverScroller(layout.getContext());
- }
-
- mScroller.fling(
- 0, getTopAndBottomOffset(), // curr
- 0, Math.round(velocityY), // velocity.
- 0, 0, // x
- minOffset, maxOffset); // y
-
- if (mScroller.computeScrollOffset()) {
- mFlingRunnable = new FlingRunnable(coordinatorLayout, layout);
- ViewCompat.postOnAnimation(layout, mFlingRunnable);
- return true;
- } else {
- onFlingFinished(coordinatorLayout, layout);
- return false;
- }
- }
-
- /**
- * Called when a fling has finished, or the fling was initiated but there wasn't enough
- * velocity to start it.
- */
- void onFlingFinished(CoordinatorLayout parent, V layout) {
- // no-op
- }
-
- /**
- * Return true if the view can be dragged.
- */
- boolean canDragView(V view) {
- return false;
- }
-
- /**
- * Returns the maximum px offset when {@code view} is being dragged.
- */
- int getMaxDragOffset(V view) {
- return -view.getHeight();
- }
-
- int getScrollRangeForDragFling(V view) {
- return view.getHeight();
- }
-
- private void ensureVelocityTracker() {
- if (mVelocityTracker == null) {
- mVelocityTracker = VelocityTracker.obtain();
- }
- }
-
- private class FlingRunnable implements Runnable {
- private final CoordinatorLayout mParent;
- private final V mLayout;
-
- FlingRunnable(CoordinatorLayout parent, V layout) {
- mParent = parent;
- mLayout = layout;
- }
-
- @Override
- public void run() {
- if (mLayout != null && mScroller != null) {
- if (mScroller.computeScrollOffset()) {
- setHeaderTopBottomOffset(mParent, mLayout, mScroller.getCurrY());
- // Post ourselves so that we run on the next animation
- ViewCompat.postOnAnimation(mLayout, this);
- } else {
- onFlingFinished(mParent, mLayout);
- }
- }
- }
- }
-}
diff --git a/android/support/design/widget/HeaderScrollingViewBehavior.java b/android/support/design/widget/HeaderScrollingViewBehavior.java
deleted file mode 100644
index 81ddde5..0000000
--- a/android/support/design/widget/HeaderScrollingViewBehavior.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.support.design.widget.CoordinatorLayout.Behavior;
-import android.support.v4.math.MathUtils;
-import android.support.v4.view.GravityCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.WindowInsetsCompat;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-
-import java.util.List;
-
-/**
- * The {@link Behavior} for a scrolling view that is positioned vertically below another view.
- * See {@link HeaderBehavior}.
- */
-abstract class HeaderScrollingViewBehavior extends ViewOffsetBehavior<View> {
-
- final Rect mTempRect1 = new Rect();
- final Rect mTempRect2 = new Rect();
-
- private int mVerticalLayoutGap = 0;
- private int mOverlayTop;
-
- public HeaderScrollingViewBehavior() {}
-
- public HeaderScrollingViewBehavior(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public boolean onMeasureChild(CoordinatorLayout parent, View child,
- int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
- int heightUsed) {
- final int childLpHeight = child.getLayoutParams().height;
- if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
- || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
- // If the menu's height is set to match_parent/wrap_content then measure it
- // with the maximum visible height
-
- final List<View> dependencies = parent.getDependencies(child);
- final View header = findFirstDependency(dependencies);
- if (header != null) {
- if (ViewCompat.getFitsSystemWindows(header)
- && !ViewCompat.getFitsSystemWindows(child)) {
- // If the header is fitting system windows then we need to also,
- // otherwise we'll get CoL's compatible measuring
- ViewCompat.setFitsSystemWindows(child, true);
-
- if (ViewCompat.getFitsSystemWindows(child)) {
- // If the set succeeded, trigger a new layout and return true
- child.requestLayout();
- return true;
- }
- }
-
- int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
- if (availableHeight == 0) {
- // If the measure spec doesn't specify a size, use the current height
- availableHeight = parent.getHeight();
- }
-
- final int height = availableHeight - header.getMeasuredHeight()
- + getScrollRange(header);
- final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,
- childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
- ? View.MeasureSpec.EXACTLY
- : View.MeasureSpec.AT_MOST);
-
- // Now measure the scrolling view with the correct height
- parent.onMeasureChild(child, parentWidthMeasureSpec,
- widthUsed, heightMeasureSpec, heightUsed);
-
- return true;
- }
- }
- return false;
- }
-
- @Override
- protected void layoutChild(final CoordinatorLayout parent, final View child,
- final int layoutDirection) {
- final List<View> dependencies = parent.getDependencies(child);
- final View header = findFirstDependency(dependencies);
-
- if (header != null) {
- final CoordinatorLayout.LayoutParams lp =
- (CoordinatorLayout.LayoutParams) child.getLayoutParams();
- final Rect available = mTempRect1;
- available.set(parent.getPaddingLeft() + lp.leftMargin,
- header.getBottom() + lp.topMargin,
- parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
- parent.getHeight() + header.getBottom()
- - parent.getPaddingBottom() - lp.bottomMargin);
-
- final WindowInsetsCompat parentInsets = parent.getLastWindowInsets();
- if (parentInsets != null && ViewCompat.getFitsSystemWindows(parent)
- && !ViewCompat.getFitsSystemWindows(child)) {
- // If we're set to handle insets but this child isn't, then it has been measured as
- // if there are no insets. We need to lay it out to match horizontally.
- // Top and bottom and already handled in the logic above
- available.left += parentInsets.getSystemWindowInsetLeft();
- available.right -= parentInsets.getSystemWindowInsetRight();
- }
-
- final Rect out = mTempRect2;
- GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
- child.getMeasuredHeight(), available, out, layoutDirection);
-
- final int overlap = getOverlapPixelsForOffset(header);
-
- child.layout(out.left, out.top - overlap, out.right, out.bottom - overlap);
- mVerticalLayoutGap = out.top - header.getBottom();
- } else {
- // If we don't have a dependency, let super handle it
- super.layoutChild(parent, child, layoutDirection);
- mVerticalLayoutGap = 0;
- }
- }
-
- float getOverlapRatioForOffset(final View header) {
- return 1f;
- }
-
- final int getOverlapPixelsForOffset(final View header) {
- return mOverlayTop == 0 ? 0 : MathUtils.clamp(
- (int) (getOverlapRatioForOffset(header) * mOverlayTop), 0, mOverlayTop);
- }
-
- private static int resolveGravity(int gravity) {
- return gravity == Gravity.NO_GRAVITY ? GravityCompat.START | Gravity.TOP : gravity;
- }
-
- abstract View findFirstDependency(List<View> views);
-
- int getScrollRange(View v) {
- return v.getMeasuredHeight();
- }
-
- /**
- * The gap between the top of the scrolling view and the bottom of the header layout in pixels.
- */
- final int getVerticalLayoutGap() {
- return mVerticalLayoutGap;
- }
-
- /**
- * Set the distance that this view should overlap any {@link AppBarLayout}.
- *
- * @param overlayTop the distance in px
- */
- public final void setOverlayTop(int overlayTop) {
- mOverlayTop = overlayTop;
- }
-
- /**
- * Returns the distance that this view should overlap any {@link AppBarLayout}.
- */
- public final int getOverlayTop() {
- return mOverlayTop;
- }
-}
\ No newline at end of file
diff --git a/android/support/design/widget/NavigationView.java b/android/support/design/widget/NavigationView.java
deleted file mode 100644
index 8fc8c76..0000000
--- a/android/support/design/widget/NavigationView.java
+++ /dev/null
@@ -1,494 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.IdRes;
-import android.support.annotation.LayoutRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.StyleRes;
-import android.support.design.R;
-import android.support.design.internal.NavigationMenu;
-import android.support.design.internal.NavigationMenuPresenter;
-import android.support.design.internal.ScrimInsetsFrameLayout;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.view.AbsSavedState;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.WindowInsetsCompat;
-import android.support.v7.content.res.AppCompatResources;
-import android.support.v7.view.SupportMenuInflater;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.support.v7.widget.TintTypedArray;
-import android.util.AttributeSet;
-import android.util.TypedValue;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-
-/**
- * Represents a standard navigation menu for application. The menu contents can be populated
- * by a menu resource file.
- * <p>NavigationView is typically placed inside a {@link android.support.v4.widget.DrawerLayout}.
- * </p>
- * <pre>
- * <android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
- * xmlns:app="http://schemas.android.com/apk/res-auto"
- * android:id="@+id/drawer_layout"
- * android:layout_width="match_parent"
- * android:layout_height="match_parent"
- * android:fitsSystemWindows="true">
- *
- * <!-- Your contents -->
- *
- * <android.support.design.widget.NavigationView
- * android:id="@+id/navigation"
- * android:layout_width="wrap_content"
- * android:layout_height="match_parent"
- * android:layout_gravity="start"
- * app:menu="@menu/my_navigation_items" />
- * </android.support.v4.widget.DrawerLayout>
- * </pre>
- */
-public class NavigationView extends ScrimInsetsFrameLayout {
-
- private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
- private static final int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled};
-
- private static final int PRESENTER_NAVIGATION_VIEW_ID = 1;
-
- private final NavigationMenu mMenu;
- private final NavigationMenuPresenter mPresenter = new NavigationMenuPresenter();
-
- OnNavigationItemSelectedListener mListener;
- private int mMaxWidth;
-
- private MenuInflater mMenuInflater;
-
- public NavigationView(Context context) {
- this(context, null);
- }
-
- public NavigationView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public NavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- ThemeUtils.checkAppCompatTheme(context);
-
- // Create the menu
- mMenu = new NavigationMenu(context);
-
- // Custom attributes
- TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
- R.styleable.NavigationView, defStyleAttr,
- R.style.Widget_Design_NavigationView);
-
- ViewCompat.setBackground(
- this, a.getDrawable(R.styleable.NavigationView_android_background));
- if (a.hasValue(R.styleable.NavigationView_elevation)) {
- ViewCompat.setElevation(this, a.getDimensionPixelSize(
- R.styleable.NavigationView_elevation, 0));
- }
- ViewCompat.setFitsSystemWindows(this,
- a.getBoolean(R.styleable.NavigationView_android_fitsSystemWindows, false));
-
- mMaxWidth = a.getDimensionPixelSize(R.styleable.NavigationView_android_maxWidth, 0);
-
- final ColorStateList itemIconTint;
- if (a.hasValue(R.styleable.NavigationView_itemIconTint)) {
- itemIconTint = a.getColorStateList(R.styleable.NavigationView_itemIconTint);
- } else {
- itemIconTint = createDefaultColorStateList(android.R.attr.textColorSecondary);
- }
-
- boolean textAppearanceSet = false;
- int textAppearance = 0;
- if (a.hasValue(R.styleable.NavigationView_itemTextAppearance)) {
- textAppearance = a.getResourceId(R.styleable.NavigationView_itemTextAppearance, 0);
- textAppearanceSet = true;
- }
-
- ColorStateList itemTextColor = null;
- if (a.hasValue(R.styleable.NavigationView_itemTextColor)) {
- itemTextColor = a.getColorStateList(R.styleable.NavigationView_itemTextColor);
- }
-
- if (!textAppearanceSet && itemTextColor == null) {
- // If there isn't a text appearance set, we'll use a default text color
- itemTextColor = createDefaultColorStateList(android.R.attr.textColorPrimary);
- }
-
- final Drawable itemBackground = a.getDrawable(R.styleable.NavigationView_itemBackground);
-
- mMenu.setCallback(new MenuBuilder.Callback() {
- @Override
- public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
- return mListener != null && mListener.onNavigationItemSelected(item);
- }
-
- @Override
- public void onMenuModeChange(MenuBuilder menu) {}
- });
- mPresenter.setId(PRESENTER_NAVIGATION_VIEW_ID);
- mPresenter.initForMenu(context, mMenu);
- mPresenter.setItemIconTintList(itemIconTint);
- if (textAppearanceSet) {
- mPresenter.setItemTextAppearance(textAppearance);
- }
- mPresenter.setItemTextColor(itemTextColor);
- mPresenter.setItemBackground(itemBackground);
- mMenu.addMenuPresenter(mPresenter);
- addView((View) mPresenter.getMenuView(this));
-
- if (a.hasValue(R.styleable.NavigationView_menu)) {
- inflateMenu(a.getResourceId(R.styleable.NavigationView_menu, 0));
- }
-
- if (a.hasValue(R.styleable.NavigationView_headerLayout)) {
- inflateHeaderView(a.getResourceId(R.styleable.NavigationView_headerLayout, 0));
- }
-
- a.recycle();
- }
-
- @Override
- protected Parcelable onSaveInstanceState() {
- Parcelable superState = super.onSaveInstanceState();
- SavedState state = new SavedState(superState);
- state.menuState = new Bundle();
- mMenu.savePresenterStates(state.menuState);
- return state;
- }
-
- @Override
- protected void onRestoreInstanceState(Parcelable savedState) {
- if (!(savedState instanceof SavedState)) {
- super.onRestoreInstanceState(savedState);
- return;
- }
- SavedState state = (SavedState) savedState;
- super.onRestoreInstanceState(state.getSuperState());
- mMenu.restorePresenterStates(state.menuState);
- }
-
- /**
- * Set a listener that will be notified when a menu item is selected.
- *
- * @param listener The listener to notify
- */
- public void setNavigationItemSelectedListener(
- @Nullable OnNavigationItemSelectedListener listener) {
- mListener = listener;
- }
-
- @Override
- protected void onMeasure(int widthSpec, int heightSpec) {
- switch (MeasureSpec.getMode(widthSpec)) {
- case MeasureSpec.EXACTLY:
- // Nothing to do
- break;
- case MeasureSpec.AT_MOST:
- widthSpec = MeasureSpec.makeMeasureSpec(
- Math.min(MeasureSpec.getSize(widthSpec), mMaxWidth), MeasureSpec.EXACTLY);
- break;
- case MeasureSpec.UNSPECIFIED:
- widthSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY);
- break;
- }
- // Let super sort out the height
- super.onMeasure(widthSpec, heightSpec);
- }
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- @Override
- protected void onInsetsChanged(WindowInsetsCompat insets) {
- mPresenter.dispatchApplyWindowInsets(insets);
- }
-
- /**
- * Inflate a menu resource into this navigation view.
- *
- * <p>Existing items in the menu will not be modified or removed.</p>
- *
- * @param resId ID of a menu resource to inflate
- */
- public void inflateMenu(int resId) {
- mPresenter.setUpdateSuspended(true);
- getMenuInflater().inflate(resId, mMenu);
- mPresenter.setUpdateSuspended(false);
- mPresenter.updateMenuView(false);
- }
-
- /**
- * Returns the {@link Menu} instance associated with this navigation view.
- */
- public Menu getMenu() {
- return mMenu;
- }
-
- /**
- * Inflates a View and add it as a header of the navigation menu.
- *
- * @param res The layout resource ID.
- * @return a newly inflated View.
- */
- public View inflateHeaderView(@LayoutRes int res) {
- return mPresenter.inflateHeaderView(res);
- }
-
- /**
- * Adds a View as a header of the navigation menu.
- *
- * @param view The view to be added as a header of the navigation menu.
- */
- public void addHeaderView(@NonNull View view) {
- mPresenter.addHeaderView(view);
- }
-
- /**
- * Removes a previously-added header view.
- *
- * @param view The view to remove
- */
- public void removeHeaderView(@NonNull View view) {
- mPresenter.removeHeaderView(view);
- }
-
- /**
- * Gets the number of headers in this NavigationView.
- *
- * @return A positive integer representing the number of headers.
- */
- public int getHeaderCount() {
- return mPresenter.getHeaderCount();
- }
-
- /**
- * Gets the header view at the specified position.
- *
- * @param index The position at which to get the view from.
- * @return The header view the specified position or null if the position does not exist in this
- * NavigationView.
- */
- public View getHeaderView(int index) {
- return mPresenter.getHeaderView(index);
- }
-
- /**
- * Returns the tint which is applied to our menu items' icons.
- *
- * @see #setItemIconTintList(ColorStateList)
- *
- * @attr ref R.styleable#NavigationView_itemIconTint
- */
- @Nullable
- public ColorStateList getItemIconTintList() {
- return mPresenter.getItemTintList();
- }
-
- /**
- * Set the tint which is applied to our menu items' icons.
- *
- * @param tint the tint to apply.
- *
- * @attr ref R.styleable#NavigationView_itemIconTint
- */
- public void setItemIconTintList(@Nullable ColorStateList tint) {
- mPresenter.setItemIconTintList(tint);
- }
-
- /**
- * Returns the tint which is applied to our menu items' icons.
- *
- * @see #setItemTextColor(ColorStateList)
- *
- * @attr ref R.styleable#NavigationView_itemTextColor
- */
- @Nullable
- public ColorStateList getItemTextColor() {
- return mPresenter.getItemTextColor();
- }
-
- /**
- * Set the text color to be used on our menu items.
- *
- * @see #getItemTextColor()
- *
- * @attr ref R.styleable#NavigationView_itemTextColor
- */
- public void setItemTextColor(@Nullable ColorStateList textColor) {
- mPresenter.setItemTextColor(textColor);
- }
-
- /**
- * Returns the background drawable for our menu items.
- *
- * @see #setItemBackgroundResource(int)
- *
- * @attr ref R.styleable#NavigationView_itemBackground
- */
- @Nullable
- public Drawable getItemBackground() {
- return mPresenter.getItemBackground();
- }
-
- /**
- * Set the background of our menu items to the given resource.
- *
- * @param resId The identifier of the resource.
- *
- * @attr ref R.styleable#NavigationView_itemBackground
- */
- public void setItemBackgroundResource(@DrawableRes int resId) {
- setItemBackground(ContextCompat.getDrawable(getContext(), resId));
- }
-
- /**
- * Set the background of our menu items to a given resource. The resource should refer to
- * a Drawable object or null to use the default background set on this navigation menu.
- *
- * @attr ref R.styleable#NavigationView_itemBackground
- */
- public void setItemBackground(@Nullable Drawable itemBackground) {
- mPresenter.setItemBackground(itemBackground);
- }
-
- /**
- * Sets the currently checked item in this navigation menu.
- *
- * @param id The item ID of the currently checked item.
- */
- public void setCheckedItem(@IdRes int id) {
- MenuItem item = mMenu.findItem(id);
- if (item != null) {
- mPresenter.setCheckedItem((MenuItemImpl) item);
- }
- }
-
- /**
- * Set the text appearance of the menu items to a given resource.
- *
- * @attr ref R.styleable#NavigationView_itemTextAppearance
- */
- public void setItemTextAppearance(@StyleRes int resId) {
- mPresenter.setItemTextAppearance(resId);
- }
-
- private MenuInflater getMenuInflater() {
- if (mMenuInflater == null) {
- mMenuInflater = new SupportMenuInflater(getContext());
- }
- return mMenuInflater;
- }
-
- private ColorStateList createDefaultColorStateList(int baseColorThemeAttr) {
- final TypedValue value = new TypedValue();
- if (!getContext().getTheme().resolveAttribute(baseColorThemeAttr, value, true)) {
- return null;
- }
- ColorStateList baseColor = AppCompatResources.getColorStateList(
- getContext(), value.resourceId);
- if (!getContext().getTheme().resolveAttribute(
- android.support.v7.appcompat.R.attr.colorPrimary, value, true)) {
- return null;
- }
- int colorPrimary = value.data;
- int defaultColor = baseColor.getDefaultColor();
- return new ColorStateList(new int[][]{
- DISABLED_STATE_SET,
- CHECKED_STATE_SET,
- EMPTY_STATE_SET
- }, new int[]{
- baseColor.getColorForState(DISABLED_STATE_SET, defaultColor),
- colorPrimary,
- defaultColor
- });
- }
-
- /**
- * Listener for handling events on navigation items.
- */
- public interface OnNavigationItemSelectedListener {
-
- /**
- * Called when an item in the navigation menu is selected.
- *
- * @param item The selected item
- *
- * @return true to display the item as the selected item
- */
- public boolean onNavigationItemSelected(@NonNull MenuItem item);
- }
-
- /**
- * User interface state that is stored by NavigationView for implementing
- * onSaveInstanceState().
- */
- public static class SavedState extends AbsSavedState {
- public Bundle menuState;
-
- public SavedState(Parcel in, ClassLoader loader) {
- super(in, loader);
- menuState = in.readBundle(loader);
- }
-
- public SavedState(Parcelable superState) {
- super(superState);
- }
-
- @Override
- public void writeToParcel(@NonNull Parcel dest, int flags) {
- super.writeToParcel(dest, flags);
- dest.writeBundle(menuState);
- }
-
- public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in, ClassLoader loader) {
- return new SavedState(in, loader);
- }
-
- @Override
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in, null);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
-
-}
diff --git a/android/support/design/widget/ShadowDrawableWrapper.java b/android/support/design/widget/ShadowDrawableWrapper.java
deleted file mode 100644
index dfb8e1d..0000000
--- a/android/support/design/widget/ShadowDrawableWrapper.java
+++ /dev/null
@@ -1,365 +0,0 @@
-/*
- * Copyright (C) 2014 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.support.design.widget;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.LinearGradient;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.PixelFormat;
-import android.graphics.RadialGradient;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.Shader;
-import android.graphics.drawable.Drawable;
-import android.support.design.R;
-import android.support.v4.content.ContextCompat;
-import android.support.v7.graphics.drawable.DrawableWrapper;
-
-/**
- * A {@link android.graphics.drawable.Drawable} which wraps another drawable and
- * draws a shadow around it.
- */
-class ShadowDrawableWrapper extends DrawableWrapper {
- // used to calculate content padding
- static final double COS_45 = Math.cos(Math.toRadians(45));
-
- static final float SHADOW_MULTIPLIER = 1.5f;
-
- static final float SHADOW_TOP_SCALE = 0.25f;
- static final float SHADOW_HORIZ_SCALE = 0.5f;
- static final float SHADOW_BOTTOM_SCALE = 1f;
-
- final Paint mCornerShadowPaint;
- final Paint mEdgeShadowPaint;
-
- final RectF mContentBounds;
-
- float mCornerRadius;
-
- Path mCornerShadowPath;
-
- // updated value with inset
- float mMaxShadowSize;
- // actual value set by developer
- float mRawMaxShadowSize;
-
- // multiplied value to account for shadow offset
- float mShadowSize;
- // actual value set by developer
- float mRawShadowSize;
-
- private boolean mDirty = true;
-
- private final int mShadowStartColor;
- private final int mShadowMiddleColor;
- private final int mShadowEndColor;
-
- private boolean mAddPaddingForCorners = true;
-
- private float mRotation;
-
- /**
- * If shadow size is set to a value above max shadow, we print a warning
- */
- private boolean mPrintedShadowClipWarning = false;
-
- public ShadowDrawableWrapper(Context context, Drawable content, float radius,
- float shadowSize, float maxShadowSize) {
- super(content);
-
- mShadowStartColor = ContextCompat.getColor(context, R.color.design_fab_shadow_start_color);
- mShadowMiddleColor = ContextCompat.getColor(context, R.color.design_fab_shadow_mid_color);
- mShadowEndColor = ContextCompat.getColor(context, R.color.design_fab_shadow_end_color);
-
- mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
- mCornerShadowPaint.setStyle(Paint.Style.FILL);
- mCornerRadius = Math.round(radius);
- mContentBounds = new RectF();
- mEdgeShadowPaint = new Paint(mCornerShadowPaint);
- mEdgeShadowPaint.setAntiAlias(false);
- setShadowSize(shadowSize, maxShadowSize);
- }
-
- /**
- * Casts the value to an even integer.
- */
- private static int toEven(float value) {
- int i = Math.round(value);
- return (i % 2 == 1) ? i - 1 : i;
- }
-
- public void setAddPaddingForCorners(boolean addPaddingForCorners) {
- mAddPaddingForCorners = addPaddingForCorners;
- invalidateSelf();
- }
-
- @Override
- public void setAlpha(int alpha) {
- super.setAlpha(alpha);
- mCornerShadowPaint.setAlpha(alpha);
- mEdgeShadowPaint.setAlpha(alpha);
- }
-
- @Override
- protected void onBoundsChange(Rect bounds) {
- mDirty = true;
- }
-
- void setShadowSize(float shadowSize, float maxShadowSize) {
- if (shadowSize < 0 || maxShadowSize < 0) {
- throw new IllegalArgumentException("invalid shadow size");
- }
- shadowSize = toEven(shadowSize);
- maxShadowSize = toEven(maxShadowSize);
- if (shadowSize > maxShadowSize) {
- shadowSize = maxShadowSize;
- if (!mPrintedShadowClipWarning) {
- mPrintedShadowClipWarning = true;
- }
- }
- if (mRawShadowSize == shadowSize && mRawMaxShadowSize == maxShadowSize) {
- return;
- }
- mRawShadowSize = shadowSize;
- mRawMaxShadowSize = maxShadowSize;
- mShadowSize = Math.round(shadowSize * SHADOW_MULTIPLIER);
- mMaxShadowSize = maxShadowSize;
- mDirty = true;
- invalidateSelf();
- }
-
- @Override
- public boolean getPadding(Rect padding) {
- int vOffset = (int) Math.ceil(calculateVerticalPadding(mRawMaxShadowSize, mCornerRadius,
- mAddPaddingForCorners));
- int hOffset = (int) Math.ceil(calculateHorizontalPadding(mRawMaxShadowSize, mCornerRadius,
- mAddPaddingForCorners));
- padding.set(hOffset, vOffset, hOffset, vOffset);
- return true;
- }
-
- public static float calculateVerticalPadding(float maxShadowSize, float cornerRadius,
- boolean addPaddingForCorners) {
- if (addPaddingForCorners) {
- return (float) (maxShadowSize * SHADOW_MULTIPLIER + (1 - COS_45) * cornerRadius);
- } else {
- return maxShadowSize * SHADOW_MULTIPLIER;
- }
- }
-
- public static float calculateHorizontalPadding(float maxShadowSize, float cornerRadius,
- boolean addPaddingForCorners) {
- if (addPaddingForCorners) {
- return (float) (maxShadowSize + (1 - COS_45) * cornerRadius);
- } else {
- return maxShadowSize;
- }
- }
-
- @Override
- public int getOpacity() {
- return PixelFormat.TRANSLUCENT;
- }
-
- public void setCornerRadius(float radius) {
- radius = Math.round(radius);
- if (mCornerRadius == radius) {
- return;
- }
- mCornerRadius = radius;
- mDirty = true;
- invalidateSelf();
- }
-
- @Override
- public void draw(Canvas canvas) {
- if (mDirty) {
- buildComponents(getBounds());
- mDirty = false;
- }
- drawShadow(canvas);
-
- super.draw(canvas);
- }
-
- final void setRotation(float rotation) {
- if (mRotation != rotation) {
- mRotation = rotation;
- invalidateSelf();
- }
- }
-
- private void drawShadow(Canvas canvas) {
- final int rotateSaved = canvas.save();
- canvas.rotate(mRotation, mContentBounds.centerX(), mContentBounds.centerY());
-
- final float edgeShadowTop = -mCornerRadius - mShadowSize;
- final float shadowOffset = mCornerRadius;
- final boolean drawHorizontalEdges = mContentBounds.width() - 2 * shadowOffset > 0;
- final boolean drawVerticalEdges = mContentBounds.height() - 2 * shadowOffset > 0;
-
- final float shadowOffsetTop = mRawShadowSize - (mRawShadowSize * SHADOW_TOP_SCALE);
- final float shadowOffsetHorizontal = mRawShadowSize - (mRawShadowSize * SHADOW_HORIZ_SCALE);
- final float shadowOffsetBottom = mRawShadowSize - (mRawShadowSize * SHADOW_BOTTOM_SCALE);
-
- final float shadowScaleHorizontal = shadowOffset / (shadowOffset + shadowOffsetHorizontal);
- final float shadowScaleTop = shadowOffset / (shadowOffset + shadowOffsetTop);
- final float shadowScaleBottom = shadowOffset / (shadowOffset + shadowOffsetBottom);
-
- // LT
- int saved = canvas.save();
- canvas.translate(mContentBounds.left + shadowOffset, mContentBounds.top + shadowOffset);
- canvas.scale(shadowScaleHorizontal, shadowScaleTop);
- canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
- if (drawHorizontalEdges) {
- // TE
- canvas.scale(1f / shadowScaleHorizontal, 1f);
- canvas.drawRect(0, edgeShadowTop,
- mContentBounds.width() - 2 * shadowOffset, -mCornerRadius,
- mEdgeShadowPaint);
- }
- canvas.restoreToCount(saved);
- // RB
- saved = canvas.save();
- canvas.translate(mContentBounds.right - shadowOffset, mContentBounds.bottom - shadowOffset);
- canvas.scale(shadowScaleHorizontal, shadowScaleBottom);
- canvas.rotate(180f);
- canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
- if (drawHorizontalEdges) {
- // BE
- canvas.scale(1f / shadowScaleHorizontal, 1f);
- canvas.drawRect(0, edgeShadowTop,
- mContentBounds.width() - 2 * shadowOffset, -mCornerRadius + mShadowSize,
- mEdgeShadowPaint);
- }
- canvas.restoreToCount(saved);
- // LB
- saved = canvas.save();
- canvas.translate(mContentBounds.left + shadowOffset, mContentBounds.bottom - shadowOffset);
- canvas.scale(shadowScaleHorizontal, shadowScaleBottom);
- canvas.rotate(270f);
- canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
- if (drawVerticalEdges) {
- // LE
- canvas.scale(1f / shadowScaleBottom, 1f);
- canvas.drawRect(0, edgeShadowTop,
- mContentBounds.height() - 2 * shadowOffset, -mCornerRadius, mEdgeShadowPaint);
- }
- canvas.restoreToCount(saved);
- // RT
- saved = canvas.save();
- canvas.translate(mContentBounds.right - shadowOffset, mContentBounds.top + shadowOffset);
- canvas.scale(shadowScaleHorizontal, shadowScaleTop);
- canvas.rotate(90f);
- canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
- if (drawVerticalEdges) {
- // RE
- canvas.scale(1f / shadowScaleTop, 1f);
- canvas.drawRect(0, edgeShadowTop,
- mContentBounds.height() - 2 * shadowOffset, -mCornerRadius, mEdgeShadowPaint);
- }
- canvas.restoreToCount(saved);
-
- canvas.restoreToCount(rotateSaved);
- }
-
- private void buildShadowCorners() {
- RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius);
- RectF outerBounds = new RectF(innerBounds);
- outerBounds.inset(-mShadowSize, -mShadowSize);
-
- if (mCornerShadowPath == null) {
- mCornerShadowPath = new Path();
- } else {
- mCornerShadowPath.reset();
- }
- mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD);
- mCornerShadowPath.moveTo(-mCornerRadius, 0);
- mCornerShadowPath.rLineTo(-mShadowSize, 0);
- // outer arc
- mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false);
- // inner arc
- mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false);
- mCornerShadowPath.close();
-
- float shadowRadius = -outerBounds.top;
- if (shadowRadius > 0f) {
- float startRatio = mCornerRadius / shadowRadius;
- float midRatio = startRatio + ((1f - startRatio) / 2f);
- mCornerShadowPaint.setShader(new RadialGradient(0, 0, shadowRadius,
- new int[]{0, mShadowStartColor, mShadowMiddleColor, mShadowEndColor},
- new float[]{0f, startRatio, midRatio, 1f},
- Shader.TileMode.CLAMP));
- }
-
- // we offset the content shadowSize/2 pixels up to make it more realistic.
- // this is why edge shadow shader has some extra space
- // When drawing bottom edge shadow, we use that extra space.
- mEdgeShadowPaint.setShader(new LinearGradient(0, innerBounds.top, 0, outerBounds.top,
- new int[]{mShadowStartColor, mShadowMiddleColor, mShadowEndColor},
- new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP));
- mEdgeShadowPaint.setAntiAlias(false);
- }
-
- private void buildComponents(Rect bounds) {
- // Card is offset SHADOW_MULTIPLIER * maxShadowSize to account for the shadow shift.
- // We could have different top-bottom offsets to avoid extra gap above but in that case
- // center aligning Views inside the CardView would be problematic.
- final float verticalOffset = mRawMaxShadowSize * SHADOW_MULTIPLIER;
- mContentBounds.set(bounds.left + mRawMaxShadowSize, bounds.top + verticalOffset,
- bounds.right - mRawMaxShadowSize, bounds.bottom - verticalOffset);
-
- getWrappedDrawable().setBounds((int) mContentBounds.left, (int) mContentBounds.top,
- (int) mContentBounds.right, (int) mContentBounds.bottom);
-
- buildShadowCorners();
- }
-
- public float getCornerRadius() {
- return mCornerRadius;
- }
-
- public void setShadowSize(float size) {
- setShadowSize(size, mRawMaxShadowSize);
- }
-
- public void setMaxShadowSize(float size) {
- setShadowSize(mRawShadowSize, size);
- }
-
- public float getShadowSize() {
- return mRawShadowSize;
- }
-
- public float getMaxShadowSize() {
- return mRawMaxShadowSize;
- }
-
- public float getMinWidth() {
- final float content = 2 *
- Math.max(mRawMaxShadowSize, mCornerRadius + mRawMaxShadowSize / 2);
- return content + mRawMaxShadowSize * 2;
- }
-
- public float getMinHeight() {
- final float content = 2 * Math.max(mRawMaxShadowSize, mCornerRadius
- + mRawMaxShadowSize * SHADOW_MULTIPLIER / 2);
- return content + (mRawMaxShadowSize * SHADOW_MULTIPLIER) * 2;
- }
-}
diff --git a/android/support/design/widget/ShadowViewDelegate.java b/android/support/design/widget/ShadowViewDelegate.java
deleted file mode 100644
index 83a3a7a..0000000
--- a/android/support/design/widget/ShadowViewDelegate.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.graphics.drawable.Drawable;
-
-interface ShadowViewDelegate {
- float getRadius();
- void setShadowPadding(int left, int top, int right, int bottom);
- void setBackgroundDrawable(Drawable background);
- boolean isCompatPaddingEnabled();
-}
diff --git a/android/support/design/widget/Snackbar.java b/android/support/design/widget/Snackbar.java
deleted file mode 100644
index bd5ffba..0000000
--- a/android/support/design/widget/Snackbar.java
+++ /dev/null
@@ -1,353 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.support.annotation.ColorInt;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.StringRes;
-import android.support.design.R;
-import android.support.design.internal.SnackbarContentLayout;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-/**
- * Snackbars provide lightweight feedback about an operation. They show a brief message at the
- * bottom of the screen on mobile and lower left on larger devices. Snackbars appear above all other
- * elements on screen and only one can be displayed at a time.
- * <p>
- * They automatically disappear after a timeout or after user interaction elsewhere on the screen,
- * particularly after interactions that summon a new surface or activity. Snackbars can be swiped
- * off screen.
- * <p>
- * Snackbars can contain an action which is set via
- * {@link #setAction(CharSequence, android.view.View.OnClickListener)}.
- * <p>
- * To be notified when a snackbar has been shown or dismissed, you can provide a {@link Callback}
- * via {@link BaseTransientBottomBar#addCallback(BaseCallback)}.</p>
- */
-public final class Snackbar extends BaseTransientBottomBar<Snackbar> {
-
- /**
- * Show the Snackbar indefinitely. This means that the Snackbar will be displayed from the time
- * that is {@link #show() shown} until either it is dismissed, or another Snackbar is shown.
- *
- * @see #setDuration
- */
- public static final int LENGTH_INDEFINITE = BaseTransientBottomBar.LENGTH_INDEFINITE;
-
- /**
- * Show the Snackbar for a short period of time.
- *
- * @see #setDuration
- */
- public static final int LENGTH_SHORT = BaseTransientBottomBar.LENGTH_SHORT;
-
- /**
- * Show the Snackbar for a long period of time.
- *
- * @see #setDuration
- */
- public static final int LENGTH_LONG = BaseTransientBottomBar.LENGTH_LONG;
-
- /**
- * Callback class for {@link Snackbar} instances.
- *
- * Note: this class is here to provide backwards-compatible way for apps written before
- * the existence of the base {@link BaseTransientBottomBar} class.
- *
- * @see BaseTransientBottomBar#addCallback(BaseCallback)
- */
- public static class Callback extends BaseCallback<Snackbar> {
- /** Indicates that the Snackbar was dismissed via a swipe.*/
- public static final int DISMISS_EVENT_SWIPE = BaseCallback.DISMISS_EVENT_SWIPE;
- /** Indicates that the Snackbar was dismissed via an action click.*/
- public static final int DISMISS_EVENT_ACTION = BaseCallback.DISMISS_EVENT_ACTION;
- /** Indicates that the Snackbar was dismissed via a timeout.*/
- public static final int DISMISS_EVENT_TIMEOUT = BaseCallback.DISMISS_EVENT_TIMEOUT;
- /** Indicates that the Snackbar was dismissed via a call to {@link #dismiss()}.*/
- public static final int DISMISS_EVENT_MANUAL = BaseCallback.DISMISS_EVENT_MANUAL;
- /** Indicates that the Snackbar was dismissed from a new Snackbar being shown.*/
- public static final int DISMISS_EVENT_CONSECUTIVE = BaseCallback.DISMISS_EVENT_CONSECUTIVE;
-
- @Override
- public void onShown(Snackbar sb) {
- // Stub implementation to make API check happy.
- }
-
- @Override
- public void onDismissed(Snackbar transientBottomBar, @DismissEvent int event) {
- // Stub implementation to make API check happy.
- }
- }
-
- @Nullable private BaseCallback<Snackbar> mCallback;
-
- private Snackbar(ViewGroup parent, View content, ContentViewCallback contentViewCallback) {
- super(parent, content, contentViewCallback);
- }
-
- /**
- * Make a Snackbar to display a message
- *
- * <p>Snackbar will try and find a parent view to hold Snackbar's view from the value given
- * to {@code view}. Snackbar will walk up the view tree trying to find a suitable parent,
- * which is defined as a {@link CoordinatorLayout} or the window decor's content view,
- * whichever comes first.
- *
- * <p>Having a {@link CoordinatorLayout} in your view hierarchy allows Snackbar to enable
- * certain features, such as swipe-to-dismiss and automatically moving of widgets like
- * {@link FloatingActionButton}.
- *
- * @param view The view to find a parent from.
- * @param text The text to show. Can be formatted text.
- * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or {@link
- * #LENGTH_LONG}
- */
- @NonNull
- public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
- @Duration int duration) {
- final ViewGroup parent = findSuitableParent(view);
- if (parent == null) {
- throw new IllegalArgumentException("No suitable parent found from the given view. "
- + "Please provide a valid view.");
- }
-
- final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
- final SnackbarContentLayout content =
- (SnackbarContentLayout) inflater.inflate(
- R.layout.design_layout_snackbar_include, parent, false);
- final Snackbar snackbar = new Snackbar(parent, content, content);
- snackbar.setText(text);
- snackbar.setDuration(duration);
- return snackbar;
- }
-
- /**
- * Make a Snackbar to display a message.
- *
- * <p>Snackbar will try and find a parent view to hold Snackbar's view from the value given
- * to {@code view}. Snackbar will walk up the view tree trying to find a suitable parent,
- * which is defined as a {@link CoordinatorLayout} or the window decor's content view,
- * whichever comes first.
- *
- * <p>Having a {@link CoordinatorLayout} in your view hierarchy allows Snackbar to enable
- * certain features, such as swipe-to-dismiss and automatically moving of widgets like
- * {@link FloatingActionButton}.
- *
- * @param view The view to find a parent from.
- * @param resId The resource id of the string resource to use. Can be formatted text.
- * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or {@link
- * #LENGTH_LONG}
- */
- @NonNull
- public static Snackbar make(@NonNull View view, @StringRes int resId, @Duration int duration) {
- return make(view, view.getResources().getText(resId), duration);
- }
-
- private static ViewGroup findSuitableParent(View view) {
- ViewGroup fallback = null;
- do {
- if (view instanceof CoordinatorLayout) {
- // We've found a CoordinatorLayout, use it
- return (ViewGroup) view;
- } else if (view instanceof FrameLayout) {
- if (view.getId() == android.R.id.content) {
- // If we've hit the decor content view, then we didn't find a CoL in the
- // hierarchy, so use it.
- return (ViewGroup) view;
- } else {
- // It's not the content view but we'll use it as our fallback
- fallback = (ViewGroup) view;
- }
- }
-
- if (view != null) {
- // Else, we will loop and crawl up the view hierarchy and try to find a parent
- final ViewParent parent = view.getParent();
- view = parent instanceof View ? (View) parent : null;
- }
- } while (view != null);
-
- // If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
- return fallback;
- }
-
- /**
- * Update the text in this {@link Snackbar}.
- *
- * @param message The new text for this {@link BaseTransientBottomBar}.
- */
- @NonNull
- public Snackbar setText(@NonNull CharSequence message) {
- final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
- final TextView tv = contentLayout.getMessageView();
- tv.setText(message);
- return this;
- }
-
- /**
- * Update the text in this {@link Snackbar}.
- *
- * @param resId The new text for this {@link BaseTransientBottomBar}.
- */
- @NonNull
- public Snackbar setText(@StringRes int resId) {
- return setText(getContext().getText(resId));
- }
-
- /**
- * Set the action to be displayed in this {@link BaseTransientBottomBar}.
- *
- * @param resId String resource to display for the action
- * @param listener callback to be invoked when the action is clicked
- */
- @NonNull
- public Snackbar setAction(@StringRes int resId, View.OnClickListener listener) {
- return setAction(getContext().getText(resId), listener);
- }
-
- /**
- * Set the action to be displayed in this {@link BaseTransientBottomBar}.
- *
- * @param text Text to display for the action
- * @param listener callback to be invoked when the action is clicked
- */
- @NonNull
- public Snackbar setAction(CharSequence text, final View.OnClickListener listener) {
- final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
- final TextView tv = contentLayout.getActionView();
-
- if (TextUtils.isEmpty(text) || listener == null) {
- tv.setVisibility(View.GONE);
- tv.setOnClickListener(null);
- } else {
- tv.setVisibility(View.VISIBLE);
- tv.setText(text);
- tv.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- listener.onClick(view);
- // Now dismiss the Snackbar
- dispatchDismiss(BaseCallback.DISMISS_EVENT_ACTION);
- }
- });
- }
- return this;
- }
-
- /**
- * Sets the text color of the action specified in
- * {@link #setAction(CharSequence, View.OnClickListener)}.
- */
- @NonNull
- public Snackbar setActionTextColor(ColorStateList colors) {
- final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
- final TextView tv = contentLayout.getActionView();
- tv.setTextColor(colors);
- return this;
- }
-
- /**
- * Sets the text color of the action specified in
- * {@link #setAction(CharSequence, View.OnClickListener)}.
- */
- @NonNull
- public Snackbar setActionTextColor(@ColorInt int color) {
- final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
- final TextView tv = contentLayout.getActionView();
- tv.setTextColor(color);
- return this;
- }
-
- /**
- * Set a callback to be called when this the visibility of this {@link Snackbar}
- * changes. Note that this method is deprecated
- * and you should use {@link #addCallback(BaseCallback)} to add a callback and
- * {@link #removeCallback(BaseCallback)} to remove a registered callback.
- *
- * @param callback Callback to notify when transient bottom bar events occur.
- * @deprecated Use {@link #addCallback(BaseCallback)}
- * @see Callback
- * @see #addCallback(BaseCallback)
- * @see #removeCallback(BaseCallback)
- */
- @Deprecated
- @NonNull
- public Snackbar setCallback(Callback callback) {
- // The logic in this method emulates what we had before support for multiple
- // registered callbacks.
- if (mCallback != null) {
- removeCallback(mCallback);
- }
- if (callback != null) {
- addCallback(callback);
- }
- // Update the deprecated field so that we can remove the passed callback the next
- // time we're called
- mCallback = callback;
- return this;
- }
-
- /**
- * @hide
- *
- * Note: this class is here to provide backwards-compatible way for apps written before
- * the existence of the base {@link BaseTransientBottomBar} class.
- */
- @RestrictTo(LIBRARY_GROUP)
- public static final class SnackbarLayout extends BaseTransientBottomBar.SnackbarBaseLayout {
- public SnackbarLayout(Context context) {
- super(context);
- }
-
- public SnackbarLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- // Work around our backwards-compatible refactoring of Snackbar and inner content
- // being inflated against snackbar's parent (instead of against the snackbar itself).
- // Every child that is width=MATCH_PARENT is remeasured again and given the full width
- // minus the paddings.
- int childCount = getChildCount();
- int availableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
- for (int i = 0; i < childCount; i++) {
- View child = getChildAt(i);
- if (child.getLayoutParams().width == ViewGroup.LayoutParams.MATCH_PARENT) {
- child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(),
- MeasureSpec.EXACTLY));
- }
- }
- }
- }
-}
-
diff --git a/android/support/design/widget/SnackbarManager.java b/android/support/design/widget/SnackbarManager.java
deleted file mode 100644
index 43892d3..0000000
--- a/android/support/design/widget/SnackbarManager.java
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-
-import java.lang.ref.WeakReference;
-
-/**
- * Manages {@link Snackbar}s.
- */
-class SnackbarManager {
-
- static final int MSG_TIMEOUT = 0;
-
- private static final int SHORT_DURATION_MS = 1500;
- private static final int LONG_DURATION_MS = 2750;
-
- private static SnackbarManager sSnackbarManager;
-
- static SnackbarManager getInstance() {
- if (sSnackbarManager == null) {
- sSnackbarManager = new SnackbarManager();
- }
- return sSnackbarManager;
- }
-
- private final Object mLock;
- private final Handler mHandler;
-
- private SnackbarRecord mCurrentSnackbar;
- private SnackbarRecord mNextSnackbar;
-
- private SnackbarManager() {
- mLock = new Object();
- mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
- @Override
- public boolean handleMessage(Message message) {
- switch (message.what) {
- case MSG_TIMEOUT:
- handleTimeout((SnackbarRecord) message.obj);
- return true;
- }
- return false;
- }
- });
- }
-
- interface Callback {
- void show();
- void dismiss(int event);
- }
-
- public void show(int duration, Callback callback) {
- synchronized (mLock) {
- if (isCurrentSnackbarLocked(callback)) {
- // Means that the callback is already in the queue. We'll just update the duration
- mCurrentSnackbar.duration = duration;
-
- // If this is the Snackbar currently being shown, call re-schedule it's
- // timeout
- mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
- scheduleTimeoutLocked(mCurrentSnackbar);
- return;
- } else if (isNextSnackbarLocked(callback)) {
- // We'll just update the duration
- mNextSnackbar.duration = duration;
- } else {
- // Else, we need to create a new record and queue it
- mNextSnackbar = new SnackbarRecord(duration, callback);
- }
-
- if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,
- Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {
- // If we currently have a Snackbar, try and cancel it and wait in line
- return;
- } else {
- // Clear out the current snackbar
- mCurrentSnackbar = null;
- // Otherwise, just show it now
- showNextSnackbarLocked();
- }
- }
- }
-
- public void dismiss(Callback callback, int event) {
- synchronized (mLock) {
- if (isCurrentSnackbarLocked(callback)) {
- cancelSnackbarLocked(mCurrentSnackbar, event);
- } else if (isNextSnackbarLocked(callback)) {
- cancelSnackbarLocked(mNextSnackbar, event);
- }
- }
- }
-
- /**
- * Should be called when a Snackbar is no longer displayed. This is after any exit
- * animation has finished.
- */
- public void onDismissed(Callback callback) {
- synchronized (mLock) {
- if (isCurrentSnackbarLocked(callback)) {
- // If the callback is from a Snackbar currently show, remove it and show a new one
- mCurrentSnackbar = null;
- if (mNextSnackbar != null) {
- showNextSnackbarLocked();
- }
- }
- }
- }
-
- /**
- * Should be called when a Snackbar is being shown. This is after any entrance animation has
- * finished.
- */
- public void onShown(Callback callback) {
- synchronized (mLock) {
- if (isCurrentSnackbarLocked(callback)) {
- scheduleTimeoutLocked(mCurrentSnackbar);
- }
- }
- }
-
- public void pauseTimeout(Callback callback) {
- synchronized (mLock) {
- if (isCurrentSnackbarLocked(callback) && !mCurrentSnackbar.paused) {
- mCurrentSnackbar.paused = true;
- mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
- }
- }
- }
-
- public void restoreTimeoutIfPaused(Callback callback) {
- synchronized (mLock) {
- if (isCurrentSnackbarLocked(callback) && mCurrentSnackbar.paused) {
- mCurrentSnackbar.paused = false;
- scheduleTimeoutLocked(mCurrentSnackbar);
- }
- }
- }
-
- public boolean isCurrent(Callback callback) {
- synchronized (mLock) {
- return isCurrentSnackbarLocked(callback);
- }
- }
-
- public boolean isCurrentOrNext(Callback callback) {
- synchronized (mLock) {
- return isCurrentSnackbarLocked(callback) || isNextSnackbarLocked(callback);
- }
- }
-
- private static class SnackbarRecord {
- final WeakReference<Callback> callback;
- int duration;
- boolean paused;
-
- SnackbarRecord(int duration, Callback callback) {
- this.callback = new WeakReference<>(callback);
- this.duration = duration;
- }
-
- boolean isSnackbar(Callback callback) {
- return callback != null && this.callback.get() == callback;
- }
- }
-
- private void showNextSnackbarLocked() {
- if (mNextSnackbar != null) {
- mCurrentSnackbar = mNextSnackbar;
- mNextSnackbar = null;
-
- final Callback callback = mCurrentSnackbar.callback.get();
- if (callback != null) {
- callback.show();
- } else {
- // The callback doesn't exist any more, clear out the Snackbar
- mCurrentSnackbar = null;
- }
- }
- }
-
- private boolean cancelSnackbarLocked(SnackbarRecord record, int event) {
- final Callback callback = record.callback.get();
- if (callback != null) {
- // Make sure we remove any timeouts for the SnackbarRecord
- mHandler.removeCallbacksAndMessages(record);
- callback.dismiss(event);
- return true;
- }
- return false;
- }
-
- private boolean isCurrentSnackbarLocked(Callback callback) {
- return mCurrentSnackbar != null && mCurrentSnackbar.isSnackbar(callback);
- }
-
- private boolean isNextSnackbarLocked(Callback callback) {
- return mNextSnackbar != null && mNextSnackbar.isSnackbar(callback);
- }
-
- private void scheduleTimeoutLocked(SnackbarRecord r) {
- if (r.duration == Snackbar.LENGTH_INDEFINITE) {
- // If we're set to indefinite, we don't want to set a timeout
- return;
- }
-
- int durationMs = LONG_DURATION_MS;
- if (r.duration > 0) {
- durationMs = r.duration;
- } else if (r.duration == Snackbar.LENGTH_SHORT) {
- durationMs = SHORT_DURATION_MS;
- }
- mHandler.removeCallbacksAndMessages(r);
- mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r), durationMs);
- }
-
- void handleTimeout(SnackbarRecord record) {
- synchronized (mLock) {
- if (mCurrentSnackbar == record || mNextSnackbar == record) {
- cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT);
- }
- }
- }
-
-}
diff --git a/android/support/design/widget/StateListAnimator.java b/android/support/design/widget/StateListAnimator.java
deleted file mode 100644
index aef24be..0000000
--- a/android/support/design/widget/StateListAnimator.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.util.StateSet;
-
-import java.util.ArrayList;
-
-final class StateListAnimator {
-
- private final ArrayList<Tuple> mTuples = new ArrayList<>();
-
- private Tuple mLastMatch = null;
- ValueAnimator mRunningAnimator = null;
-
- private final ValueAnimator.AnimatorListener mAnimationListener =
- new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- if (mRunningAnimator == animator) {
- mRunningAnimator = null;
- }
- }
- };
-
- /**
- * Associates the given Animation with the provided drawable state specs so that it will be run
- * when the View's drawable state matches the specs.
- *
- * @param specs The drawable state specs to match against
- * @param animator The animator to run when the specs match
- */
- public void addState(int[] specs, ValueAnimator animator) {
- Tuple tuple = new Tuple(specs, animator);
- animator.addListener(mAnimationListener);
- mTuples.add(tuple);
- }
-
- /**
- * Called by View
- */
- void setState(int[] state) {
- Tuple match = null;
- final int count = mTuples.size();
- for (int i = 0; i < count; i++) {
- final Tuple tuple = mTuples.get(i);
- if (StateSet.stateSetMatches(tuple.mSpecs, state)) {
- match = tuple;
- break;
- }
- }
- if (match == mLastMatch) {
- return;
- }
- if (mLastMatch != null) {
- cancel();
- }
-
- mLastMatch = match;
-
- if (match != null) {
- start(match);
- }
- }
-
- private void start(Tuple match) {
- mRunningAnimator = match.mAnimator;
- mRunningAnimator.start();
- }
-
- private void cancel() {
- if (mRunningAnimator != null) {
- mRunningAnimator.cancel();
- mRunningAnimator = null;
- }
- }
-
- /**
- * If there is an animation running for a recent state change, ends it.
- *
- * <p>This causes the animation to assign the end value(s) to the View.</p>
- */
- public void jumpToCurrentState() {
- if (mRunningAnimator != null) {
- mRunningAnimator.end();
- mRunningAnimator = null;
- }
- }
-
- static class Tuple {
- final int[] mSpecs;
- final ValueAnimator mAnimator;
-
- Tuple(int[] specs, ValueAnimator animator) {
- mSpecs = specs;
- mAnimator = animator;
- }
- }
-}
\ No newline at end of file
diff --git a/android/support/design/widget/SwipeDismissBehavior.java b/android/support/design/widget/SwipeDismissBehavior.java
deleted file mode 100644
index d857334..0000000
--- a/android/support/design/widget/SwipeDismissBehavior.java
+++ /dev/null
@@ -1,411 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.widget.ViewDragHelper;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * An interaction behavior plugin for child views of {@link CoordinatorLayout} to provide support
- * for the 'swipe-to-dismiss' gesture.
- */
-public class SwipeDismissBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
-
- /**
- * A view is not currently being dragged or animating as a result of a fling/snap.
- */
- public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE;
-
- /**
- * A view is currently being dragged. The position is currently changing as a result
- * of user input or simulated user input.
- */
- public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING;
-
- /**
- * A view is currently settling into place as a result of a fling or
- * predefined non-interactive motion.
- */
- public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING;
-
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef({SWIPE_DIRECTION_START_TO_END, SWIPE_DIRECTION_END_TO_START, SWIPE_DIRECTION_ANY})
- @Retention(RetentionPolicy.SOURCE)
- private @interface SwipeDirection {}
-
- /**
- * Swipe direction that only allows swiping in the direction of start-to-end. That is
- * left-to-right in LTR, or right-to-left in RTL.
- */
- public static final int SWIPE_DIRECTION_START_TO_END = 0;
-
- /**
- * Swipe direction that only allows swiping in the direction of end-to-start. That is
- * right-to-left in LTR or left-to-right in RTL.
- */
- public static final int SWIPE_DIRECTION_END_TO_START = 1;
-
- /**
- * Swipe direction which allows swiping in either direction.
- */
- public static final int SWIPE_DIRECTION_ANY = 2;
-
- private static final float DEFAULT_DRAG_DISMISS_THRESHOLD = 0.5f;
- private static final float DEFAULT_ALPHA_START_DISTANCE = 0f;
- private static final float DEFAULT_ALPHA_END_DISTANCE = DEFAULT_DRAG_DISMISS_THRESHOLD;
-
- ViewDragHelper mViewDragHelper;
- OnDismissListener mListener;
- private boolean mInterceptingEvents;
-
- private float mSensitivity = 0f;
- private boolean mSensitivitySet;
-
- int mSwipeDirection = SWIPE_DIRECTION_ANY;
- float mDragDismissThreshold = DEFAULT_DRAG_DISMISS_THRESHOLD;
- float mAlphaStartSwipeDistance = DEFAULT_ALPHA_START_DISTANCE;
- float mAlphaEndSwipeDistance = DEFAULT_ALPHA_END_DISTANCE;
-
- /**
- * Callback interface used to notify the application that the view has been dismissed.
- */
- public interface OnDismissListener {
- /**
- * Called when {@code view} has been dismissed via swiping.
- */
- public void onDismiss(View view);
-
- /**
- * Called when the drag state has changed.
- *
- * @param state the new state. One of
- * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
- */
- public void onDragStateChanged(int state);
- }
-
- /**
- * Set the listener to be used when a dismiss event occurs.
- *
- * @param listener the listener to use.
- */
- public void setListener(OnDismissListener listener) {
- mListener = listener;
- }
-
- /**
- * Sets the swipe direction for this behavior.
- *
- * @param direction one of the {@link #SWIPE_DIRECTION_START_TO_END},
- * {@link #SWIPE_DIRECTION_END_TO_START} or {@link #SWIPE_DIRECTION_ANY}
- */
- public void setSwipeDirection(@SwipeDirection int direction) {
- mSwipeDirection = direction;
- }
-
- /**
- * Set the threshold for telling if a view has been dragged enough to be dismissed.
- *
- * @param distance a ratio of a view's width, values are clamped to 0 >= x <= 1f;
- */
- public void setDragDismissDistance(float distance) {
- mDragDismissThreshold = clamp(0f, distance, 1f);
- }
-
- /**
- * The minimum swipe distance before the view's alpha is modified.
- *
- * @param fraction the distance as a fraction of the view's width.
- */
- public void setStartAlphaSwipeDistance(float fraction) {
- mAlphaStartSwipeDistance = clamp(0f, fraction, 1f);
- }
-
- /**
- * The maximum swipe distance for the view's alpha is modified.
- *
- * @param fraction the distance as a fraction of the view's width.
- */
- public void setEndAlphaSwipeDistance(float fraction) {
- mAlphaEndSwipeDistance = clamp(0f, fraction, 1f);
- }
-
- /**
- * Set the sensitivity used for detecting the start of a swipe. This only takes effect if
- * no touch handling has occured yet.
- *
- * @param sensitivity Multiplier for how sensitive we should be about detecting
- * the start of a drag. Larger values are more sensitive. 1.0f is normal.
- */
- public void setSensitivity(float sensitivity) {
- mSensitivity = sensitivity;
- mSensitivitySet = true;
- }
-
- @Override
- public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
- boolean dispatchEventToHelper = mInterceptingEvents;
-
- switch (event.getActionMasked()) {
- case MotionEvent.ACTION_DOWN:
- mInterceptingEvents = parent.isPointInChildBounds(child,
- (int) event.getX(), (int) event.getY());
- dispatchEventToHelper = mInterceptingEvents;
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- // Reset the ignore flag for next time
- mInterceptingEvents = false;
- break;
- }
-
- if (dispatchEventToHelper) {
- ensureViewDragHelper(parent);
- return mViewDragHelper.shouldInterceptTouchEvent(event);
- }
- return false;
- }
-
- @Override
- public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
- if (mViewDragHelper != null) {
- mViewDragHelper.processTouchEvent(event);
- return true;
- }
- return false;
- }
-
- /**
- * Called when the user's input indicates that they want to swipe the given view.
- *
- * @param view View the user is attempting to swipe
- * @return true if the view can be dismissed via swiping, false otherwise
- */
- public boolean canSwipeDismissView(@NonNull View view) {
- return true;
- }
-
- private final ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
- private static final int INVALID_POINTER_ID = -1;
-
- private int mOriginalCapturedViewLeft;
- private int mActivePointerId = INVALID_POINTER_ID;
-
- @Override
- public boolean tryCaptureView(View child, int pointerId) {
- // Only capture if we don't already have an active pointer id
- return mActivePointerId == INVALID_POINTER_ID && canSwipeDismissView(child);
- }
-
- @Override
- public void onViewCaptured(View capturedChild, int activePointerId) {
- mActivePointerId = activePointerId;
- mOriginalCapturedViewLeft = capturedChild.getLeft();
-
- // The view has been captured, and thus a drag is about to start so stop any parents
- // intercepting
- final ViewParent parent = capturedChild.getParent();
- if (parent != null) {
- parent.requestDisallowInterceptTouchEvent(true);
- }
- }
-
- @Override
- public void onViewDragStateChanged(int state) {
- if (mListener != null) {
- mListener.onDragStateChanged(state);
- }
- }
-
- @Override
- public void onViewReleased(View child, float xvel, float yvel) {
- // Reset the active pointer ID
- mActivePointerId = INVALID_POINTER_ID;
-
- final int childWidth = child.getWidth();
- int targetLeft;
- boolean dismiss = false;
-
- if (shouldDismiss(child, xvel)) {
- targetLeft = child.getLeft() < mOriginalCapturedViewLeft
- ? mOriginalCapturedViewLeft - childWidth
- : mOriginalCapturedViewLeft + childWidth;
- dismiss = true;
- } else {
- // Else, reset back to the original left
- targetLeft = mOriginalCapturedViewLeft;
- }
-
- if (mViewDragHelper.settleCapturedViewAt(targetLeft, child.getTop())) {
- ViewCompat.postOnAnimation(child, new SettleRunnable(child, dismiss));
- } else if (dismiss && mListener != null) {
- mListener.onDismiss(child);
- }
- }
-
- private boolean shouldDismiss(View child, float xvel) {
- if (xvel != 0f) {
- final boolean isRtl = ViewCompat.getLayoutDirection(child)
- == ViewCompat.LAYOUT_DIRECTION_RTL;
-
- if (mSwipeDirection == SWIPE_DIRECTION_ANY) {
- // We don't care about the direction so return true
- return true;
- } else if (mSwipeDirection == SWIPE_DIRECTION_START_TO_END) {
- // We only allow start-to-end swiping, so the fling needs to be in the
- // correct direction
- return isRtl ? xvel < 0f : xvel > 0f;
- } else if (mSwipeDirection == SWIPE_DIRECTION_END_TO_START) {
- // We only allow end-to-start swiping, so the fling needs to be in the
- // correct direction
- return isRtl ? xvel > 0f : xvel < 0f;
- }
- } else {
- final int distance = child.getLeft() - mOriginalCapturedViewLeft;
- final int thresholdDistance = Math.round(child.getWidth() * mDragDismissThreshold);
- return Math.abs(distance) >= thresholdDistance;
- }
-
- return false;
- }
-
- @Override
- public int getViewHorizontalDragRange(View child) {
- return child.getWidth();
- }
-
- @Override
- public int clampViewPositionHorizontal(View child, int left, int dx) {
- final boolean isRtl = ViewCompat.getLayoutDirection(child)
- == ViewCompat.LAYOUT_DIRECTION_RTL;
- int min, max;
-
- if (mSwipeDirection == SWIPE_DIRECTION_START_TO_END) {
- if (isRtl) {
- min = mOriginalCapturedViewLeft - child.getWidth();
- max = mOriginalCapturedViewLeft;
- } else {
- min = mOriginalCapturedViewLeft;
- max = mOriginalCapturedViewLeft + child.getWidth();
- }
- } else if (mSwipeDirection == SWIPE_DIRECTION_END_TO_START) {
- if (isRtl) {
- min = mOriginalCapturedViewLeft;
- max = mOriginalCapturedViewLeft + child.getWidth();
- } else {
- min = mOriginalCapturedViewLeft - child.getWidth();
- max = mOriginalCapturedViewLeft;
- }
- } else {
- min = mOriginalCapturedViewLeft - child.getWidth();
- max = mOriginalCapturedViewLeft + child.getWidth();
- }
-
- return clamp(min, left, max);
- }
-
- @Override
- public int clampViewPositionVertical(View child, int top, int dy) {
- return child.getTop();
- }
-
- @Override
- public void onViewPositionChanged(View child, int left, int top, int dx, int dy) {
- final float startAlphaDistance = mOriginalCapturedViewLeft
- + child.getWidth() * mAlphaStartSwipeDistance;
- final float endAlphaDistance = mOriginalCapturedViewLeft
- + child.getWidth() * mAlphaEndSwipeDistance;
-
- if (left <= startAlphaDistance) {
- child.setAlpha(1f);
- } else if (left >= endAlphaDistance) {
- child.setAlpha(0f);
- } else {
- // We're between the start and end distances
- final float distance = fraction(startAlphaDistance, endAlphaDistance, left);
- child.setAlpha(clamp(0f, 1f - distance, 1f));
- }
- }
- };
-
- private void ensureViewDragHelper(ViewGroup parent) {
- if (mViewDragHelper == null) {
- mViewDragHelper = mSensitivitySet
- ? ViewDragHelper.create(parent, mSensitivity, mDragCallback)
- : ViewDragHelper.create(parent, mDragCallback);
- }
- }
-
- private class SettleRunnable implements Runnable {
- private final View mView;
- private final boolean mDismiss;
-
- SettleRunnable(View view, boolean dismiss) {
- mView = view;
- mDismiss = dismiss;
- }
-
- @Override
- public void run() {
- if (mViewDragHelper != null && mViewDragHelper.continueSettling(true)) {
- ViewCompat.postOnAnimation(mView, this);
- } else {
- if (mDismiss && mListener != null) {
- mListener.onDismiss(mView);
- }
- }
- }
- }
-
- static float clamp(float min, float value, float max) {
- return Math.min(Math.max(min, value), max);
- }
-
- static int clamp(int min, int value, int max) {
- return Math.min(Math.max(min, value), max);
- }
-
- /**
- * Retrieve the current drag state of this behavior. This will return one of
- * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
- *
- * @return The current drag state
- */
- public int getDragState() {
- return mViewDragHelper != null ? mViewDragHelper.getViewDragState() : STATE_IDLE;
- }
-
- /**
- * The fraction that {@code value} is between {@code startValue} and {@code endValue}.
- */
- static float fraction(float startValue, float endValue, float value) {
- return (value - startValue) / (endValue - startValue);
- }
-}
\ No newline at end of file
diff --git a/android/support/design/widget/TabItem.java b/android/support/design/widget/TabItem.java
deleted file mode 100644
index 09b01db..0000000
--- a/android/support/design/widget/TabItem.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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.support.design.widget;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.support.design.R;
-import android.support.v7.widget.TintTypedArray;
-import android.util.AttributeSet;
-import android.view.View;
-
-/**
- * TabItem is a special 'view' which allows you to declare tab items for a {@link TabLayout}
- * within a layout. This view is not actually added to TabLayout, it is just a dummy which allows
- * setting of a tab items's text, icon and custom layout. See TabLayout for more information on how
- * to use it.
- *
- * @attr ref android.support.design.R.styleable#TabItem_android_icon
- * @attr ref android.support.design.R.styleable#TabItem_android_text
- * @attr ref android.support.design.R.styleable#TabItem_android_layout
- *
- * @see TabLayout
- */
-public final class TabItem extends View {
- final CharSequence mText;
- final Drawable mIcon;
- final int mCustomLayout;
-
- public TabItem(Context context) {
- this(context, null);
- }
-
- public TabItem(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
- R.styleable.TabItem);
- mText = a.getText(R.styleable.TabItem_android_text);
- mIcon = a.getDrawable(R.styleable.TabItem_android_icon);
- mCustomLayout = a.getResourceId(R.styleable.TabItem_android_layout, 0);
- a.recycle();
- }
-}
\ No newline at end of file
diff --git a/android/support/design/widget/TabLayout.java b/android/support/design/widget/TabLayout.java
deleted file mode 100644
index 9b81465..0000000
--- a/android/support/design/widget/TabLayout.java
+++ /dev/null
@@ -1,2217 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import static android.support.v4.view.ViewPager.SCROLL_STATE_DRAGGING;
-import static android.support.v4.view.ViewPager.SCROLL_STATE_IDLE;
-import static android.support.v4.view.ViewPager.SCROLL_STATE_SETTLING;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.database.DataSetObserver;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.support.annotation.ColorInt;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.IntDef;
-import android.support.annotation.LayoutRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.StringRes;
-import android.support.design.R;
-import android.support.v4.util.Pools;
-import android.support.v4.view.GravityCompat;
-import android.support.v4.view.PagerAdapter;
-import android.support.v4.view.PointerIconCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.ViewPager;
-import android.support.v4.widget.TextViewCompat;
-import android.support.v7.app.ActionBar;
-import android.support.v7.content.res.AppCompatResources;
-import android.support.v7.widget.TooltipCompat;
-import android.text.Layout;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.SoundEffectConstants;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.widget.HorizontalScrollView;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Iterator;
-
-/**
- * TabLayout provides a horizontal layout to display tabs.
- *
- * <p>Population of the tabs to display is
- * done through {@link Tab} instances. You create tabs via {@link #newTab()}. From there you can
- * change the tab's label or icon via {@link Tab#setText(int)} and {@link Tab#setIcon(int)}
- * respectively. To display the tab, you need to add it to the layout via one of the
- * {@link #addTab(Tab)} methods. For example:
- * <pre>
- * TabLayout tabLayout = ...;
- * tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
- * tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
- * tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));
- * </pre>
- * You should set a listener via {@link #setOnTabSelectedListener(OnTabSelectedListener)} to be
- * notified when any tab's selection state has been changed.
- *
- * <p>You can also add items to TabLayout in your layout through the use of {@link TabItem}.
- * An example usage is like so:</p>
- *
- * <pre>
- * <android.support.design.widget.TabLayout
- * android:layout_height="wrap_content"
- * android:layout_width="match_parent">
- *
- * <android.support.design.widget.TabItem
- * android:text="@string/tab_text"/>
- *
- * <android.support.design.widget.TabItem
- * android:icon="@drawable/ic_android"/>
- *
- * </android.support.design.widget.TabLayout>
- * </pre>
- *
- * <h3>ViewPager integration</h3>
- * <p>
- * If you're using a {@link android.support.v4.view.ViewPager} together
- * with this layout, you can call {@link #setupWithViewPager(ViewPager)} to link the two together.
- * This layout will be automatically populated from the {@link PagerAdapter}'s page titles.</p>
- *
- * <p>
- * This view also supports being used as part of a ViewPager's decor, and can be added
- * directly to the ViewPager in a layout resource file like so:</p>
- *
- * <pre>
- * <android.support.v4.view.ViewPager
- * android:layout_width="match_parent"
- * android:layout_height="match_parent">
- *
- * <android.support.design.widget.TabLayout
- * android:layout_width="match_parent"
- * android:layout_height="wrap_content"
- * android:layout_gravity="top" />
- *
- * </android.support.v4.view.ViewPager>
- * </pre>
- *
- * @see <a href="http://www.google.com/design/spec/components/tabs.html">Tabs</a>
- *
- * @attr ref android.support.design.R.styleable#TabLayout_tabPadding
- * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingStart
- * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingTop
- * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingEnd
- * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingBottom
- * @attr ref android.support.design.R.styleable#TabLayout_tabContentStart
- * @attr ref android.support.design.R.styleable#TabLayout_tabBackground
- * @attr ref android.support.design.R.styleable#TabLayout_tabMinWidth
- * @attr ref android.support.design.R.styleable#TabLayout_tabMaxWidth
- * @attr ref android.support.design.R.styleable#TabLayout_tabTextAppearance
- */
[email protected]
-public class TabLayout extends HorizontalScrollView {
-
- private static final int DEFAULT_HEIGHT_WITH_TEXT_ICON = 72; // dps
- static final int DEFAULT_GAP_TEXT_ICON = 8; // dps
- private static final int INVALID_WIDTH = -1;
- private static final int DEFAULT_HEIGHT = 48; // dps
- private static final int TAB_MIN_WIDTH_MARGIN = 56; //dps
- static final int FIXED_WRAP_GUTTER_MIN = 16; //dps
- static final int MOTION_NON_ADJACENT_OFFSET = 24;
-
- private static final int ANIMATION_DURATION = 300;
-
- private static final Pools.Pool<Tab> sTabPool = new Pools.SynchronizedPool<>(16);
-
- /**
- * Scrollable tabs display a subset of tabs at any given moment, and can contain longer tab
- * labels and a larger number of tabs. They are best used for browsing contexts in touch
- * interfaces when users don’t need to directly compare the tab labels.
- *
- * @see #setTabMode(int)
- * @see #getTabMode()
- */
- public static final int MODE_SCROLLABLE = 0;
-
- /**
- * Fixed tabs display all tabs concurrently and are best used with content that benefits from
- * quick pivots between tabs. The maximum number of tabs is limited by the view’s width.
- * Fixed tabs have equal width, based on the widest tab label.
- *
- * @see #setTabMode(int)
- * @see #getTabMode()
- */
- public static final int MODE_FIXED = 1;
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef(value = {MODE_SCROLLABLE, MODE_FIXED})
- @Retention(RetentionPolicy.SOURCE)
- public @interface Mode {}
-
- /**
- * Gravity used to fill the {@link TabLayout} as much as possible. This option only takes effect
- * when used with {@link #MODE_FIXED}.
- *
- * @see #setTabGravity(int)
- * @see #getTabGravity()
- */
- public static final int GRAVITY_FILL = 0;
-
- /**
- * Gravity used to lay out the tabs in the center of the {@link TabLayout}.
- *
- * @see #setTabGravity(int)
- * @see #getTabGravity()
- */
- public static final int GRAVITY_CENTER = 1;
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef(flag = true, value = {GRAVITY_FILL, GRAVITY_CENTER})
- @Retention(RetentionPolicy.SOURCE)
- public @interface TabGravity {}
-
- /**
- * Callback interface invoked when a tab's selection state changes.
- */
- public interface OnTabSelectedListener {
-
- /**
- * Called when a tab enters the selected state.
- *
- * @param tab The tab that was selected
- */
- public void onTabSelected(Tab tab);
-
- /**
- * Called when a tab exits the selected state.
- *
- * @param tab The tab that was unselected
- */
- public void onTabUnselected(Tab tab);
-
- /**
- * Called when a tab that is already selected is chosen again by the user. Some applications
- * may use this action to return to the top level of a category.
- *
- * @param tab The tab that was reselected.
- */
- public void onTabReselected(Tab tab);
- }
-
- private final ArrayList<Tab> mTabs = new ArrayList<>();
- private Tab mSelectedTab;
-
- private final SlidingTabStrip mTabStrip;
-
- int mTabPaddingStart;
- int mTabPaddingTop;
- int mTabPaddingEnd;
- int mTabPaddingBottom;
-
- int mTabTextAppearance;
- ColorStateList mTabTextColors;
- float mTabTextSize;
- float mTabTextMultiLineSize;
-
- final int mTabBackgroundResId;
-
- int mTabMaxWidth = Integer.MAX_VALUE;
- private final int mRequestedTabMinWidth;
- private final int mRequestedTabMaxWidth;
- private final int mScrollableTabMinWidth;
-
- private int mContentInsetStart;
-
- int mTabGravity;
- int mMode;
-
- private OnTabSelectedListener mSelectedListener;
- private final ArrayList<OnTabSelectedListener> mSelectedListeners = new ArrayList<>();
- private OnTabSelectedListener mCurrentVpSelectedListener;
-
- private ValueAnimator mScrollAnimator;
-
- ViewPager mViewPager;
- private PagerAdapter mPagerAdapter;
- private DataSetObserver mPagerAdapterObserver;
- private TabLayoutOnPageChangeListener mPageChangeListener;
- private AdapterChangeListener mAdapterChangeListener;
- private boolean mSetupViewPagerImplicitly;
-
- // Pool we use as a simple RecyclerBin
- private final Pools.Pool<TabView> mTabViewPool = new Pools.SimplePool<>(12);
-
- public TabLayout(Context context) {
- this(context, null);
- }
-
- public TabLayout(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- ThemeUtils.checkAppCompatTheme(context);
-
- // Disable the Scroll Bar
- setHorizontalScrollBarEnabled(false);
-
- // Add the TabStrip
- mTabStrip = new SlidingTabStrip(context);
- super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams(
- LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout,
- defStyleAttr, R.style.Widget_Design_TabLayout);
-
- mTabStrip.setSelectedIndicatorHeight(
- a.getDimensionPixelSize(R.styleable.TabLayout_tabIndicatorHeight, 0));
- mTabStrip.setSelectedIndicatorColor(a.getColor(R.styleable.TabLayout_tabIndicatorColor, 0));
-
- mTabPaddingStart = mTabPaddingTop = mTabPaddingEnd = mTabPaddingBottom = a
- .getDimensionPixelSize(R.styleable.TabLayout_tabPadding, 0);
- mTabPaddingStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingStart,
- mTabPaddingStart);
- mTabPaddingTop = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingTop,
- mTabPaddingTop);
- mTabPaddingEnd = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingEnd,
- mTabPaddingEnd);
- mTabPaddingBottom = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingBottom,
- mTabPaddingBottom);
-
- mTabTextAppearance = a.getResourceId(R.styleable.TabLayout_tabTextAppearance,
- R.style.TextAppearance_Design_Tab);
-
- // Text colors/sizes come from the text appearance first
- final TypedArray ta = context.obtainStyledAttributes(mTabTextAppearance,
- android.support.v7.appcompat.R.styleable.TextAppearance);
- try {
- mTabTextSize = ta.getDimensionPixelSize(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize, 0);
- mTabTextColors = ta.getColorStateList(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor);
- } finally {
- ta.recycle();
- }
-
- if (a.hasValue(R.styleable.TabLayout_tabTextColor)) {
- // If we have an explicit text color set, use it instead
- mTabTextColors = a.getColorStateList(R.styleable.TabLayout_tabTextColor);
- }
-
- if (a.hasValue(R.styleable.TabLayout_tabSelectedTextColor)) {
- // We have an explicit selected text color set, so we need to make merge it with the
- // current colors. This is exposed so that developers can use theme attributes to set
- // this (theme attrs in ColorStateLists are Lollipop+)
- final int selected = a.getColor(R.styleable.TabLayout_tabSelectedTextColor, 0);
- mTabTextColors = createColorStateList(mTabTextColors.getDefaultColor(), selected);
- }
-
- mRequestedTabMinWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMinWidth,
- INVALID_WIDTH);
- mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMaxWidth,
- INVALID_WIDTH);
- mTabBackgroundResId = a.getResourceId(R.styleable.TabLayout_tabBackground, 0);
- mContentInsetStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabContentStart, 0);
- mMode = a.getInt(R.styleable.TabLayout_tabMode, MODE_FIXED);
- mTabGravity = a.getInt(R.styleable.TabLayout_tabGravity, GRAVITY_FILL);
- a.recycle();
-
- // TODO add attr for these
- final Resources res = getResources();
- mTabTextMultiLineSize = res.getDimensionPixelSize(R.dimen.design_tab_text_size_2line);
- mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_tab_scrollable_min_width);
-
- // Now apply the tab mode and gravity
- applyModeAndGravity();
- }
-
- /**
- * Sets the tab indicator's color for the currently selected tab.
- *
- * @param color color to use for the indicator
- *
- * @attr ref android.support.design.R.styleable#TabLayout_tabIndicatorColor
- */
- public void setSelectedTabIndicatorColor(@ColorInt int color) {
- mTabStrip.setSelectedIndicatorColor(color);
- }
-
- /**
- * Sets the tab indicator's height for the currently selected tab.
- *
- * @param height height to use for the indicator in pixels
- *
- * @attr ref android.support.design.R.styleable#TabLayout_tabIndicatorHeight
- */
- public void setSelectedTabIndicatorHeight(int height) {
- mTabStrip.setSelectedIndicatorHeight(height);
- }
-
- /**
- * Set the scroll position of the tabs. This is useful for when the tabs are being displayed as
- * part of a scrolling container such as {@link android.support.v4.view.ViewPager}.
- * <p>
- * Calling this method does not update the selected tab, it is only used for drawing purposes.
- *
- * @param position current scroll position
- * @param positionOffset Value from [0, 1) indicating the offset from {@code position}.
- * @param updateSelectedText Whether to update the text's selected state.
- */
- public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) {
- setScrollPosition(position, positionOffset, updateSelectedText, true);
- }
-
- void setScrollPosition(int position, float positionOffset, boolean updateSelectedText,
- boolean updateIndicatorPosition) {
- final int roundedPosition = Math.round(position + positionOffset);
- if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) {
- return;
- }
-
- // Set the indicator position, if enabled
- if (updateIndicatorPosition) {
- mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset);
- }
-
- // Now update the scroll position, canceling any running animation
- if (mScrollAnimator != null && mScrollAnimator.isRunning()) {
- mScrollAnimator.cancel();
- }
- scrollTo(calculateScrollXForTab(position, positionOffset), 0);
-
- // Update the 'selected state' view as we scroll, if enabled
- if (updateSelectedText) {
- setSelectedTabView(roundedPosition);
- }
- }
-
- private float getScrollPosition() {
- return mTabStrip.getIndicatorPosition();
- }
-
- /**
- * Add a tab to this layout. The tab will be added at the end of the list.
- * If this is the first tab to be added it will become the selected tab.
- *
- * @param tab Tab to add
- */
- public void addTab(@NonNull Tab tab) {
- addTab(tab, mTabs.isEmpty());
- }
-
- /**
- * Add a tab to this layout. The tab will be inserted at <code>position</code>.
- * If this is the first tab to be added it will become the selected tab.
- *
- * @param tab The tab to add
- * @param position The new position of the tab
- */
- public void addTab(@NonNull Tab tab, int position) {
- addTab(tab, position, mTabs.isEmpty());
- }
-
- /**
- * Add a tab to this layout. The tab will be added at the end of the list.
- *
- * @param tab Tab to add
- * @param setSelected True if the added tab should become the selected tab.
- */
- public void addTab(@NonNull Tab tab, boolean setSelected) {
- addTab(tab, mTabs.size(), setSelected);
- }
-
- /**
- * Add a tab to this layout. The tab will be inserted at <code>position</code>.
- *
- * @param tab The tab to add
- * @param position The new position of the tab
- * @param setSelected True if the added tab should become the selected tab.
- */
- public void addTab(@NonNull Tab tab, int position, boolean setSelected) {
- if (tab.mParent != this) {
- throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
- }
- configureTab(tab, position);
- addTabView(tab);
-
- if (setSelected) {
- tab.select();
- }
- }
-
- private void addTabFromItemView(@NonNull TabItem item) {
- final Tab tab = newTab();
- if (item.mText != null) {
- tab.setText(item.mText);
- }
- if (item.mIcon != null) {
- tab.setIcon(item.mIcon);
- }
- if (item.mCustomLayout != 0) {
- tab.setCustomView(item.mCustomLayout);
- }
- if (!TextUtils.isEmpty(item.getContentDescription())) {
- tab.setContentDescription(item.getContentDescription());
- }
- addTab(tab);
- }
-
- /**
- * @deprecated Use {@link #addOnTabSelectedListener(OnTabSelectedListener)} and
- * {@link #removeOnTabSelectedListener(OnTabSelectedListener)}.
- */
- @Deprecated
- public void setOnTabSelectedListener(@Nullable OnTabSelectedListener listener) {
- // The logic in this method emulates what we had before support for multiple
- // registered listeners.
- if (mSelectedListener != null) {
- removeOnTabSelectedListener(mSelectedListener);
- }
- // Update the deprecated field so that we can remove the passed listener the next
- // time we're called
- mSelectedListener = listener;
- if (listener != null) {
- addOnTabSelectedListener(listener);
- }
- }
-
- /**
- * Add a {@link TabLayout.OnTabSelectedListener} that will be invoked when tab selection
- * changes.
- *
- * <p>Components that add a listener should take care to remove it when finished via
- * {@link #removeOnTabSelectedListener(OnTabSelectedListener)}.</p>
- *
- * @param listener listener to add
- */
- public void addOnTabSelectedListener(@NonNull OnTabSelectedListener listener) {
- if (!mSelectedListeners.contains(listener)) {
- mSelectedListeners.add(listener);
- }
- }
-
- /**
- * Remove the given {@link TabLayout.OnTabSelectedListener} that was previously added via
- * {@link #addOnTabSelectedListener(OnTabSelectedListener)}.
- *
- * @param listener listener to remove
- */
- public void removeOnTabSelectedListener(@NonNull OnTabSelectedListener listener) {
- mSelectedListeners.remove(listener);
- }
-
- /**
- * Remove all previously added {@link TabLayout.OnTabSelectedListener}s.
- */
- public void clearOnTabSelectedListeners() {
- mSelectedListeners.clear();
- }
-
- /**
- * Create and return a new {@link Tab}. You need to manually add this using
- * {@link #addTab(Tab)} or a related method.
- *
- * @return A new Tab
- * @see #addTab(Tab)
- */
- @NonNull
- public Tab newTab() {
- Tab tab = sTabPool.acquire();
- if (tab == null) {
- tab = new Tab();
- }
- tab.mParent = this;
- tab.mView = createTabView(tab);
- return tab;
- }
-
- /**
- * Returns the number of tabs currently registered with the action bar.
- *
- * @return Tab count
- */
- public int getTabCount() {
- return mTabs.size();
- }
-
- /**
- * Returns the tab at the specified index.
- */
- @Nullable
- public Tab getTabAt(int index) {
- return (index < 0 || index >= getTabCount()) ? null : mTabs.get(index);
- }
-
- /**
- * Returns the position of the current selected tab.
- *
- * @return selected tab position, or {@code -1} if there isn't a selected tab.
- */
- public int getSelectedTabPosition() {
- return mSelectedTab != null ? mSelectedTab.getPosition() : -1;
- }
-
- /**
- * Remove a tab from the layout. If the removed tab was selected it will be deselected
- * and another tab will be selected if present.
- *
- * @param tab The tab to remove
- */
- public void removeTab(Tab tab) {
- if (tab.mParent != this) {
- throw new IllegalArgumentException("Tab does not belong to this TabLayout.");
- }
-
- removeTabAt(tab.getPosition());
- }
-
- /**
- * Remove a tab from the layout. If the removed tab was selected it will be deselected
- * and another tab will be selected if present.
- *
- * @param position Position of the tab to remove
- */
- public void removeTabAt(int position) {
- final int selectedTabPosition = mSelectedTab != null ? mSelectedTab.getPosition() : 0;
- removeTabViewAt(position);
-
- final Tab removedTab = mTabs.remove(position);
- if (removedTab != null) {
- removedTab.reset();
- sTabPool.release(removedTab);
- }
-
- final int newTabCount = mTabs.size();
- for (int i = position; i < newTabCount; i++) {
- mTabs.get(i).setPosition(i);
- }
-
- if (selectedTabPosition == position) {
- selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1)));
- }
- }
-
- /**
- * Remove all tabs from the action bar and deselect the current tab.
- */
- public void removeAllTabs() {
- // Remove all the views
- for (int i = mTabStrip.getChildCount() - 1; i >= 0; i--) {
- removeTabViewAt(i);
- }
-
- for (final Iterator<Tab> i = mTabs.iterator(); i.hasNext();) {
- final Tab tab = i.next();
- i.remove();
- tab.reset();
- sTabPool.release(tab);
- }
-
- mSelectedTab = null;
- }
-
- /**
- * Set the behavior mode for the Tabs in this layout. The valid input options are:
- * <ul>
- * <li>{@link #MODE_FIXED}: Fixed tabs display all tabs concurrently and are best used
- * with content that benefits from quick pivots between tabs.</li>
- * <li>{@link #MODE_SCROLLABLE}: Scrollable tabs display a subset of tabs at any given moment,
- * and can contain longer tab labels and a larger number of tabs. They are best used for
- * browsing contexts in touch interfaces when users don’t need to directly compare the tab
- * labels. This mode is commonly used with a {@link android.support.v4.view.ViewPager}.</li>
- * </ul>
- *
- * @param mode one of {@link #MODE_FIXED} or {@link #MODE_SCROLLABLE}.
- *
- * @attr ref android.support.design.R.styleable#TabLayout_tabMode
- */
- public void setTabMode(@Mode int mode) {
- if (mode != mMode) {
- mMode = mode;
- applyModeAndGravity();
- }
- }
-
- /**
- * Returns the current mode used by this {@link TabLayout}.
- *
- * @see #setTabMode(int)
- */
- @Mode
- public int getTabMode() {
- return mMode;
- }
-
- /**
- * Set the gravity to use when laying out the tabs.
- *
- * @param gravity one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}.
- *
- * @attr ref android.support.design.R.styleable#TabLayout_tabGravity
- */
- public void setTabGravity(@TabGravity int gravity) {
- if (mTabGravity != gravity) {
- mTabGravity = gravity;
- applyModeAndGravity();
- }
- }
-
- /**
- * The current gravity used for laying out tabs.
- *
- * @return one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}.
- */
- @TabGravity
- public int getTabGravity() {
- return mTabGravity;
- }
-
- /**
- * Sets the text colors for the different states (normal, selected) used for the tabs.
- *
- * @see #getTabTextColors()
- */
- public void setTabTextColors(@Nullable ColorStateList textColor) {
- if (mTabTextColors != textColor) {
- mTabTextColors = textColor;
- updateAllTabs();
- }
- }
-
- /**
- * Gets the text colors for the different states (normal, selected) used for the tabs.
- */
- @Nullable
- public ColorStateList getTabTextColors() {
- return mTabTextColors;
- }
-
- /**
- * Sets the text colors for the different states (normal, selected) used for the tabs.
- *
- * @attr ref android.support.design.R.styleable#TabLayout_tabTextColor
- * @attr ref android.support.design.R.styleable#TabLayout_tabSelectedTextColor
- */
- public void setTabTextColors(int normalColor, int selectedColor) {
- setTabTextColors(createColorStateList(normalColor, selectedColor));
- }
-
- /**
- * The one-stop shop for setting up this {@link TabLayout} with a {@link ViewPager}.
- *
- * <p>This is the same as calling {@link #setupWithViewPager(ViewPager, boolean)} with
- * auto-refresh enabled.</p>
- *
- * @param viewPager the ViewPager to link to, or {@code null} to clear any previous link
- */
- public void setupWithViewPager(@Nullable ViewPager viewPager) {
- setupWithViewPager(viewPager, true);
- }
-
- /**
- * The one-stop shop for setting up this {@link TabLayout} with a {@link ViewPager}.
- *
- * <p>This method will link the given ViewPager and this TabLayout together so that
- * changes in one are automatically reflected in the other. This includes scroll state changes
- * and clicks. The tabs displayed in this layout will be populated
- * from the ViewPager adapter's page titles.</p>
- *
- * <p>If {@code autoRefresh} is {@code true}, any changes in the {@link PagerAdapter} will
- * trigger this layout to re-populate itself from the adapter's titles.</p>
- *
- * <p>If the given ViewPager is non-null, it needs to already have a
- * {@link PagerAdapter} set.</p>
- *
- * @param viewPager the ViewPager to link to, or {@code null} to clear any previous link
- * @param autoRefresh whether this layout should refresh its contents if the given ViewPager's
- * content changes
- */
- public void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh) {
- setupWithViewPager(viewPager, autoRefresh, false);
- }
-
- private void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh,
- boolean implicitSetup) {
- if (mViewPager != null) {
- // If we've already been setup with a ViewPager, remove us from it
- if (mPageChangeListener != null) {
- mViewPager.removeOnPageChangeListener(mPageChangeListener);
- }
- if (mAdapterChangeListener != null) {
- mViewPager.removeOnAdapterChangeListener(mAdapterChangeListener);
- }
- }
-
- if (mCurrentVpSelectedListener != null) {
- // If we already have a tab selected listener for the ViewPager, remove it
- removeOnTabSelectedListener(mCurrentVpSelectedListener);
- mCurrentVpSelectedListener = null;
- }
-
- if (viewPager != null) {
- mViewPager = viewPager;
-
- // Add our custom OnPageChangeListener to the ViewPager
- if (mPageChangeListener == null) {
- mPageChangeListener = new TabLayoutOnPageChangeListener(this);
- }
- mPageChangeListener.reset();
- viewPager.addOnPageChangeListener(mPageChangeListener);
-
- // Now we'll add a tab selected listener to set ViewPager's current item
- mCurrentVpSelectedListener = new ViewPagerOnTabSelectedListener(viewPager);
- addOnTabSelectedListener(mCurrentVpSelectedListener);
-
- final PagerAdapter adapter = viewPager.getAdapter();
- if (adapter != null) {
- // Now we'll populate ourselves from the pager adapter, adding an observer if
- // autoRefresh is enabled
- setPagerAdapter(adapter, autoRefresh);
- }
-
- // Add a listener so that we're notified of any adapter changes
- if (mAdapterChangeListener == null) {
- mAdapterChangeListener = new AdapterChangeListener();
- }
- mAdapterChangeListener.setAutoRefresh(autoRefresh);
- viewPager.addOnAdapterChangeListener(mAdapterChangeListener);
-
- // Now update the scroll position to match the ViewPager's current item
- setScrollPosition(viewPager.getCurrentItem(), 0f, true);
- } else {
- // We've been given a null ViewPager so we need to clear out the internal state,
- // listeners and observers
- mViewPager = null;
- setPagerAdapter(null, false);
- }
-
- mSetupViewPagerImplicitly = implicitSetup;
- }
-
- /**
- * @deprecated Use {@link #setupWithViewPager(ViewPager)} to link a TabLayout with a ViewPager
- * together. When that method is used, the TabLayout will be automatically updated
- * when the {@link PagerAdapter} is changed.
- */
- @Deprecated
- public void setTabsFromPagerAdapter(@Nullable final PagerAdapter adapter) {
- setPagerAdapter(adapter, false);
- }
-
- @Override
- public boolean shouldDelayChildPressedState() {
- // Only delay the pressed state if the tabs can scroll
- return getTabScrollRange() > 0;
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- if (mViewPager == null) {
- // If we don't have a ViewPager already, check if our parent is a ViewPager to
- // setup with it automatically
- final ViewParent vp = getParent();
- if (vp instanceof ViewPager) {
- // If we have a ViewPager parent and we've been added as part of its decor, let's
- // assume that we should automatically setup to display any titles
- setupWithViewPager((ViewPager) vp, true, true);
- }
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- if (mSetupViewPagerImplicitly) {
- // If we've been setup with a ViewPager implicitly, let's clear out any listeners, etc
- setupWithViewPager(null);
- mSetupViewPagerImplicitly = false;
- }
- }
-
- private int getTabScrollRange() {
- return Math.max(0, mTabStrip.getWidth() - getWidth() - getPaddingLeft()
- - getPaddingRight());
- }
-
- void setPagerAdapter(@Nullable final PagerAdapter adapter, final boolean addObserver) {
- if (mPagerAdapter != null && mPagerAdapterObserver != null) {
- // If we already have a PagerAdapter, unregister our observer
- mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver);
- }
-
- mPagerAdapter = adapter;
-
- if (addObserver && adapter != null) {
- // Register our observer on the new adapter
- if (mPagerAdapterObserver == null) {
- mPagerAdapterObserver = new PagerAdapterObserver();
- }
- adapter.registerDataSetObserver(mPagerAdapterObserver);
- }
-
- // Finally make sure we reflect the new adapter
- populateFromPagerAdapter();
- }
-
- void populateFromPagerAdapter() {
- removeAllTabs();
-
- if (mPagerAdapter != null) {
- final int adapterCount = mPagerAdapter.getCount();
- for (int i = 0; i < adapterCount; i++) {
- addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false);
- }
-
- // Make sure we reflect the currently set ViewPager item
- if (mViewPager != null && adapterCount > 0) {
- final int curItem = mViewPager.getCurrentItem();
- if (curItem != getSelectedTabPosition() && curItem < getTabCount()) {
- selectTab(getTabAt(curItem));
- }
- }
- }
- }
-
- private void updateAllTabs() {
- for (int i = 0, z = mTabs.size(); i < z; i++) {
- mTabs.get(i).updateView();
- }
- }
-
- private TabView createTabView(@NonNull final Tab tab) {
- TabView tabView = mTabViewPool != null ? mTabViewPool.acquire() : null;
- if (tabView == null) {
- tabView = new TabView(getContext());
- }
- tabView.setTab(tab);
- tabView.setFocusable(true);
- tabView.setMinimumWidth(getTabMinWidth());
- return tabView;
- }
-
- private void configureTab(Tab tab, int position) {
- tab.setPosition(position);
- mTabs.add(position, tab);
-
- final int count = mTabs.size();
- for (int i = position + 1; i < count; i++) {
- mTabs.get(i).setPosition(i);
- }
- }
-
- private void addTabView(Tab tab) {
- final TabView tabView = tab.mView;
- mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs());
- }
-
- @Override
- public void addView(View child) {
- addViewInternal(child);
- }
-
- @Override
- public void addView(View child, int index) {
- addViewInternal(child);
- }
-
- @Override
- public void addView(View child, ViewGroup.LayoutParams params) {
- addViewInternal(child);
- }
-
- @Override
- public void addView(View child, int index, ViewGroup.LayoutParams params) {
- addViewInternal(child);
- }
-
- private void addViewInternal(final View child) {
- if (child instanceof TabItem) {
- addTabFromItemView((TabItem) child);
- } else {
- throw new IllegalArgumentException("Only TabItem instances can be added to TabLayout");
- }
- }
-
- private LinearLayout.LayoutParams createLayoutParamsForTabs() {
- final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
- LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
- updateTabViewLayoutParams(lp);
- return lp;
- }
-
- private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) {
- if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) {
- lp.width = 0;
- lp.weight = 1;
- } else {
- lp.width = LinearLayout.LayoutParams.WRAP_CONTENT;
- lp.weight = 0;
- }
- }
-
- int dpToPx(int dps) {
- return Math.round(getResources().getDisplayMetrics().density * dps);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // If we have a MeasureSpec which allows us to decide our height, try and use the default
- // height
- final int idealHeight = dpToPx(getDefaultHeight()) + getPaddingTop() + getPaddingBottom();
- switch (MeasureSpec.getMode(heightMeasureSpec)) {
- case MeasureSpec.AT_MOST:
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(
- Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)),
- MeasureSpec.EXACTLY);
- break;
- case MeasureSpec.UNSPECIFIED:
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY);
- break;
- }
-
- final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
- if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
- // If we don't have an unspecified width spec, use the given size to calculate
- // the max tab width
- mTabMaxWidth = mRequestedTabMaxWidth > 0
- ? mRequestedTabMaxWidth
- : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN);
- }
-
- // Now super measure itself using the (possibly) modified height spec
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- if (getChildCount() == 1) {
- // If we're in fixed mode then we need to make the tab strip is the same width as us
- // so we don't scroll
- final View child = getChildAt(0);
- boolean remeasure = false;
-
- switch (mMode) {
- case MODE_SCROLLABLE:
- // We only need to resize the child if it's smaller than us. This is similar
- // to fillViewport
- remeasure = child.getMeasuredWidth() < getMeasuredWidth();
- break;
- case MODE_FIXED:
- // Resize the child so that it doesn't scroll
- remeasure = child.getMeasuredWidth() != getMeasuredWidth();
- break;
- }
-
- if (remeasure) {
- // Re-measure the child with a widthSpec set to be exactly our measure width
- int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop()
- + getPaddingBottom(), child.getLayoutParams().height);
- int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
- getMeasuredWidth(), MeasureSpec.EXACTLY);
- child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
- }
- }
- }
-
- private void removeTabViewAt(int position) {
- final TabView view = (TabView) mTabStrip.getChildAt(position);
- mTabStrip.removeViewAt(position);
- if (view != null) {
- view.reset();
- mTabViewPool.release(view);
- }
- requestLayout();
- }
-
- private void animateToTab(int newPosition) {
- if (newPosition == Tab.INVALID_POSITION) {
- return;
- }
-
- if (getWindowToken() == null || !ViewCompat.isLaidOut(this)
- || mTabStrip.childrenNeedLayout()) {
- // If we don't have a window token, or we haven't been laid out yet just draw the new
- // position now
- setScrollPosition(newPosition, 0f, true);
- return;
- }
-
- final int startScrollX = getScrollX();
- final int targetScrollX = calculateScrollXForTab(newPosition, 0);
-
- if (startScrollX != targetScrollX) {
- ensureScrollAnimator();
-
- mScrollAnimator.setIntValues(startScrollX, targetScrollX);
- mScrollAnimator.start();
- }
-
- // Now animate the indicator
- mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION);
- }
-
- private void ensureScrollAnimator() {
- if (mScrollAnimator == null) {
- mScrollAnimator = new ValueAnimator();
- mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
- mScrollAnimator.setDuration(ANIMATION_DURATION);
- mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- scrollTo((int) animator.getAnimatedValue(), 0);
- }
- });
- }
- }
-
- void setScrollAnimatorListener(Animator.AnimatorListener listener) {
- ensureScrollAnimator();
- mScrollAnimator.addListener(listener);
- }
-
- private void setSelectedTabView(int position) {
- final int tabCount = mTabStrip.getChildCount();
- if (position < tabCount) {
- for (int i = 0; i < tabCount; i++) {
- final View child = mTabStrip.getChildAt(i);
- child.setSelected(i == position);
- }
- }
- }
-
- void selectTab(Tab tab) {
- selectTab(tab, true);
- }
-
- void selectTab(final Tab tab, boolean updateIndicator) {
- final Tab currentTab = mSelectedTab;
-
- if (currentTab == tab) {
- if (currentTab != null) {
- dispatchTabReselected(tab);
- animateToTab(tab.getPosition());
- }
- } else {
- final int newPosition = tab != null ? tab.getPosition() : Tab.INVALID_POSITION;
- if (updateIndicator) {
- if ((currentTab == null || currentTab.getPosition() == Tab.INVALID_POSITION)
- && newPosition != Tab.INVALID_POSITION) {
- // If we don't currently have a tab, just draw the indicator
- setScrollPosition(newPosition, 0f, true);
- } else {
- animateToTab(newPosition);
- }
- if (newPosition != Tab.INVALID_POSITION) {
- setSelectedTabView(newPosition);
- }
- }
- if (currentTab != null) {
- dispatchTabUnselected(currentTab);
- }
- mSelectedTab = tab;
- if (tab != null) {
- dispatchTabSelected(tab);
- }
- }
- }
-
- private void dispatchTabSelected(@NonNull final Tab tab) {
- for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
- mSelectedListeners.get(i).onTabSelected(tab);
- }
- }
-
- private void dispatchTabUnselected(@NonNull final Tab tab) {
- for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
- mSelectedListeners.get(i).onTabUnselected(tab);
- }
- }
-
- private void dispatchTabReselected(@NonNull final Tab tab) {
- for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
- mSelectedListeners.get(i).onTabReselected(tab);
- }
- }
-
- private int calculateScrollXForTab(int position, float positionOffset) {
- if (mMode == MODE_SCROLLABLE) {
- final View selectedChild = mTabStrip.getChildAt(position);
- final View nextChild = position + 1 < mTabStrip.getChildCount()
- ? mTabStrip.getChildAt(position + 1)
- : null;
- final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0;
- final int nextWidth = nextChild != null ? nextChild.getWidth() : 0;
-
- // base scroll amount: places center of tab in center of parent
- int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (getWidth() / 2);
- // offset amount: fraction of the distance between centers of tabs
- int scrollOffset = (int) ((selectedWidth + nextWidth) * 0.5f * positionOffset);
-
- return (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR)
- ? scrollBase + scrollOffset
- : scrollBase - scrollOffset;
- }
- return 0;
- }
-
- private void applyModeAndGravity() {
- int paddingStart = 0;
- if (mMode == MODE_SCROLLABLE) {
- // If we're scrollable, or fixed at start, inset using padding
- paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart);
- }
- ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0);
-
- switch (mMode) {
- case MODE_FIXED:
- mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL);
- break;
- case MODE_SCROLLABLE:
- mTabStrip.setGravity(GravityCompat.START);
- break;
- }
-
- updateTabViews(true);
- }
-
- void updateTabViews(final boolean requestLayout) {
- for (int i = 0; i < mTabStrip.getChildCount(); i++) {
- View child = mTabStrip.getChildAt(i);
- child.setMinimumWidth(getTabMinWidth());
- updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams());
- if (requestLayout) {
- child.requestLayout();
- }
- }
- }
-
- /**
- * A tab in this layout. Instances can be created via {@link #newTab()}.
- */
- public static final class Tab {
-
- /**
- * An invalid position for a tab.
- *
- * @see #getPosition()
- */
- public static final int INVALID_POSITION = -1;
-
- private Object mTag;
- private Drawable mIcon;
- private CharSequence mText;
- private CharSequence mContentDesc;
- private int mPosition = INVALID_POSITION;
- private View mCustomView;
-
- TabLayout mParent;
- TabView mView;
-
- Tab() {
- // Private constructor
- }
-
- /**
- * @return This Tab's tag object.
- */
- @Nullable
- public Object getTag() {
- return mTag;
- }
-
- /**
- * Give this Tab an arbitrary object to hold for later use.
- *
- * @param tag Object to store
- * @return The current instance for call chaining
- */
- @NonNull
- public Tab setTag(@Nullable Object tag) {
- mTag = tag;
- return this;
- }
-
-
- /**
- * Returns the custom view used for this tab.
- *
- * @see #setCustomView(View)
- * @see #setCustomView(int)
- */
- @Nullable
- public View getCustomView() {
- return mCustomView;
- }
-
- /**
- * Set a custom view to be used for this tab.
- * <p>
- * If the provided view contains a {@link TextView} with an ID of
- * {@link android.R.id#text1} then that will be updated with the value given
- * to {@link #setText(CharSequence)}. Similarly, if this layout contains an
- * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with
- * the value given to {@link #setIcon(Drawable)}.
- * </p>
- *
- * @param view Custom view to be used as a tab.
- * @return The current instance for call chaining
- */
- @NonNull
- public Tab setCustomView(@Nullable View view) {
- mCustomView = view;
- updateView();
- return this;
- }
-
- /**
- * Set a custom view to be used for this tab.
- * <p>
- * If the inflated layout contains a {@link TextView} with an ID of
- * {@link android.R.id#text1} then that will be updated with the value given
- * to {@link #setText(CharSequence)}. Similarly, if this layout contains an
- * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with
- * the value given to {@link #setIcon(Drawable)}.
- * </p>
- *
- * @param resId A layout resource to inflate and use as a custom tab view
- * @return The current instance for call chaining
- */
- @NonNull
- public Tab setCustomView(@LayoutRes int resId) {
- final LayoutInflater inflater = LayoutInflater.from(mView.getContext());
- return setCustomView(inflater.inflate(resId, mView, false));
- }
-
- /**
- * Return the icon associated with this tab.
- *
- * @return The tab's icon
- */
- @Nullable
- public Drawable getIcon() {
- return mIcon;
- }
-
- /**
- * Return the current position of this tab in the action bar.
- *
- * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in
- * the action bar.
- */
- public int getPosition() {
- return mPosition;
- }
-
- void setPosition(int position) {
- mPosition = position;
- }
-
- /**
- * Return the text of this tab.
- *
- * @return The tab's text
- */
- @Nullable
- public CharSequence getText() {
- return mText;
- }
-
- /**
- * Set the icon displayed on this tab.
- *
- * @param icon The drawable to use as an icon
- * @return The current instance for call chaining
- */
- @NonNull
- public Tab setIcon(@Nullable Drawable icon) {
- mIcon = icon;
- updateView();
- return this;
- }
-
- /**
- * Set the icon displayed on this tab.
- *
- * @param resId A resource ID referring to the icon that should be displayed
- * @return The current instance for call chaining
- */
- @NonNull
- public Tab setIcon(@DrawableRes int resId) {
- if (mParent == null) {
- throw new IllegalArgumentException("Tab not attached to a TabLayout");
- }
- return setIcon(AppCompatResources.getDrawable(mParent.getContext(), resId));
- }
-
- /**
- * Set the text displayed on this tab. Text may be truncated if there is not room to display
- * the entire string.
- *
- * @param text The text to display
- * @return The current instance for call chaining
- */
- @NonNull
- public Tab setText(@Nullable CharSequence text) {
- mText = text;
- updateView();
- return this;
- }
-
- /**
- * Set the text displayed on this tab. Text may be truncated if there is not room to display
- * the entire string.
- *
- * @param resId A resource ID referring to the text that should be displayed
- * @return The current instance for call chaining
- */
- @NonNull
- public Tab setText(@StringRes int resId) {
- if (mParent == null) {
- throw new IllegalArgumentException("Tab not attached to a TabLayout");
- }
- return setText(mParent.getResources().getText(resId));
- }
-
- /**
- * Select this tab. Only valid if the tab has been added to the action bar.
- */
- public void select() {
- if (mParent == null) {
- throw new IllegalArgumentException("Tab not attached to a TabLayout");
- }
- mParent.selectTab(this);
- }
-
- /**
- * Returns true if this tab is currently selected.
- */
- public boolean isSelected() {
- if (mParent == null) {
- throw new IllegalArgumentException("Tab not attached to a TabLayout");
- }
- return mParent.getSelectedTabPosition() == mPosition;
- }
-
- /**
- * Set a description of this tab's content for use in accessibility support. If no content
- * description is provided the title will be used.
- *
- * @param resId A resource ID referring to the description text
- * @return The current instance for call chaining
- * @see #setContentDescription(CharSequence)
- * @see #getContentDescription()
- */
- @NonNull
- public Tab setContentDescription(@StringRes int resId) {
- if (mParent == null) {
- throw new IllegalArgumentException("Tab not attached to a TabLayout");
- }
- return setContentDescription(mParent.getResources().getText(resId));
- }
-
- /**
- * Set a description of this tab's content for use in accessibility support. If no content
- * description is provided the title will be used.
- *
- * @param contentDesc Description of this tab's content
- * @return The current instance for call chaining
- * @see #setContentDescription(int)
- * @see #getContentDescription()
- */
- @NonNull
- public Tab setContentDescription(@Nullable CharSequence contentDesc) {
- mContentDesc = contentDesc;
- updateView();
- return this;
- }
-
- /**
- * Gets a brief description of this tab's content for use in accessibility support.
- *
- * @return Description of this tab's content
- * @see #setContentDescription(CharSequence)
- * @see #setContentDescription(int)
- */
- @Nullable
- public CharSequence getContentDescription() {
- return mContentDesc;
- }
-
- void updateView() {
- if (mView != null) {
- mView.update();
- }
- }
-
- void reset() {
- mParent = null;
- mView = null;
- mTag = null;
- mIcon = null;
- mText = null;
- mContentDesc = null;
- mPosition = INVALID_POSITION;
- mCustomView = null;
- }
- }
-
- class TabView extends LinearLayout {
- private Tab mTab;
- private TextView mTextView;
- private ImageView mIconView;
-
- private View mCustomView;
- private TextView mCustomTextView;
- private ImageView mCustomIconView;
-
- private int mDefaultMaxLines = 2;
-
- public TabView(Context context) {
- super(context);
- if (mTabBackgroundResId != 0) {
- ViewCompat.setBackground(
- this, AppCompatResources.getDrawable(context, mTabBackgroundResId));
- }
- ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop,
- mTabPaddingEnd, mTabPaddingBottom);
- setGravity(Gravity.CENTER);
- setOrientation(VERTICAL);
- setClickable(true);
- ViewCompat.setPointerIcon(this,
- PointerIconCompat.getSystemIcon(getContext(), PointerIconCompat.TYPE_HAND));
- }
-
- @Override
- public boolean performClick() {
- final boolean handled = super.performClick();
-
- if (mTab != null) {
- if (!handled) {
- playSoundEffect(SoundEffectConstants.CLICK);
- }
- mTab.select();
- return true;
- } else {
- return handled;
- }
- }
-
- @Override
- public void setSelected(final boolean selected) {
- final boolean changed = isSelected() != selected;
-
- super.setSelected(selected);
-
- if (changed && selected && Build.VERSION.SDK_INT < 16) {
- // Pre-JB we need to manually send the TYPE_VIEW_SELECTED event
- sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
- }
-
- // Always dispatch this to the child views, regardless of whether the value has
- // changed
- if (mTextView != null) {
- mTextView.setSelected(selected);
- }
- if (mIconView != null) {
- mIconView.setSelected(selected);
- }
- if (mCustomView != null) {
- mCustomView.setSelected(selected);
- }
- }
-
- @Override
- public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
- super.onInitializeAccessibilityEvent(event);
- // This view masquerades as an action bar tab.
- event.setClassName(ActionBar.Tab.class.getName());
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
- super.onInitializeAccessibilityNodeInfo(info);
- // This view masquerades as an action bar tab.
- info.setClassName(ActionBar.Tab.class.getName());
- }
-
- @Override
- public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) {
- final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec);
- final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec);
- final int maxWidth = getTabMaxWidth();
-
- final int widthMeasureSpec;
- final int heightMeasureSpec = origHeightMeasureSpec;
-
- if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED
- || specWidthSize > maxWidth)) {
- // If we have a max width and a given spec which is either unspecified or
- // larger than the max width, update the width spec using the same mode
- widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, MeasureSpec.AT_MOST);
- } else {
- // Else, use the original width spec
- widthMeasureSpec = origWidthMeasureSpec;
- }
-
- // Now lets measure
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- // We need to switch the text size based on whether the text is spanning 2 lines or not
- if (mTextView != null) {
- final Resources res = getResources();
- float textSize = mTabTextSize;
- int maxLines = mDefaultMaxLines;
-
- if (mIconView != null && mIconView.getVisibility() == VISIBLE) {
- // If the icon view is being displayed, we limit the text to 1 line
- maxLines = 1;
- } else if (mTextView != null && mTextView.getLineCount() > 1) {
- // Otherwise when we have text which wraps we reduce the text size
- textSize = mTabTextMultiLineSize;
- }
-
- final float curTextSize = mTextView.getTextSize();
- final int curLineCount = mTextView.getLineCount();
- final int curMaxLines = TextViewCompat.getMaxLines(mTextView);
-
- if (textSize != curTextSize || (curMaxLines >= 0 && maxLines != curMaxLines)) {
- // We've got a new text size and/or max lines...
- boolean updateTextView = true;
-
- if (mMode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) {
- // If we're in fixed mode, going up in text size and currently have 1 line
- // then it's very easy to get into an infinite recursion.
- // To combat that we check to see if the change in text size
- // will cause a line count change. If so, abort the size change and stick
- // to the smaller size.
- final Layout layout = mTextView.getLayout();
- if (layout == null || approximateLineWidth(layout, 0, textSize)
- > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) {
- updateTextView = false;
- }
- }
-
- if (updateTextView) {
- mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
- mTextView.setMaxLines(maxLines);
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- }
- }
- }
-
- void setTab(@Nullable final Tab tab) {
- if (tab != mTab) {
- mTab = tab;
- update();
- }
- }
-
- void reset() {
- setTab(null);
- setSelected(false);
- }
-
- final void update() {
- final Tab tab = mTab;
- final View custom = tab != null ? tab.getCustomView() : null;
- if (custom != null) {
- final ViewParent customParent = custom.getParent();
- if (customParent != this) {
- if (customParent != null) {
- ((ViewGroup) customParent).removeView(custom);
- }
- addView(custom);
- }
- mCustomView = custom;
- if (mTextView != null) {
- mTextView.setVisibility(GONE);
- }
- if (mIconView != null) {
- mIconView.setVisibility(GONE);
- mIconView.setImageDrawable(null);
- }
-
- mCustomTextView = (TextView) custom.findViewById(android.R.id.text1);
- if (mCustomTextView != null) {
- mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView);
- }
- mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon);
- } else {
- // We do not have a custom view. Remove one if it already exists
- if (mCustomView != null) {
- removeView(mCustomView);
- mCustomView = null;
- }
- mCustomTextView = null;
- mCustomIconView = null;
- }
-
- if (mCustomView == null) {
- // If there isn't a custom view, we'll us our own in-built layouts
- if (mIconView == null) {
- ImageView iconView = (ImageView) LayoutInflater.from(getContext())
- .inflate(R.layout.design_layout_tab_icon, this, false);
- addView(iconView, 0);
- mIconView = iconView;
- }
- if (mTextView == null) {
- TextView textView = (TextView) LayoutInflater.from(getContext())
- .inflate(R.layout.design_layout_tab_text, this, false);
- addView(textView);
- mTextView = textView;
- mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView);
- }
- TextViewCompat.setTextAppearance(mTextView, mTabTextAppearance);
- if (mTabTextColors != null) {
- mTextView.setTextColor(mTabTextColors);
- }
- updateTextAndIcon(mTextView, mIconView);
- } else {
- // Else, we'll see if there is a TextView or ImageView present and update them
- if (mCustomTextView != null || mCustomIconView != null) {
- updateTextAndIcon(mCustomTextView, mCustomIconView);
- }
- }
-
- // Finally update our selected state
- setSelected(tab != null && tab.isSelected());
- }
-
- private void updateTextAndIcon(@Nullable final TextView textView,
- @Nullable final ImageView iconView) {
- final Drawable icon = mTab != null ? mTab.getIcon() : null;
- final CharSequence text = mTab != null ? mTab.getText() : null;
- final CharSequence contentDesc = mTab != null ? mTab.getContentDescription() : null;
-
- if (iconView != null) {
- if (icon != null) {
- iconView.setImageDrawable(icon);
- iconView.setVisibility(VISIBLE);
- setVisibility(VISIBLE);
- } else {
- iconView.setVisibility(GONE);
- iconView.setImageDrawable(null);
- }
- iconView.setContentDescription(contentDesc);
- }
-
- final boolean hasText = !TextUtils.isEmpty(text);
- if (textView != null) {
- if (hasText) {
- textView.setText(text);
- textView.setVisibility(VISIBLE);
- setVisibility(VISIBLE);
- } else {
- textView.setVisibility(GONE);
- textView.setText(null);
- }
- textView.setContentDescription(contentDesc);
- }
-
- if (iconView != null) {
- MarginLayoutParams lp = ((MarginLayoutParams) iconView.getLayoutParams());
- int bottomMargin = 0;
- if (hasText && iconView.getVisibility() == VISIBLE) {
- // If we're showing both text and icon, add some margin bottom to the icon
- bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON);
- }
- if (bottomMargin != lp.bottomMargin) {
- lp.bottomMargin = bottomMargin;
- iconView.requestLayout();
- }
- }
- TooltipCompat.setTooltipText(this, hasText ? null : contentDesc);
- }
-
- public Tab getTab() {
- return mTab;
- }
-
- /**
- * Approximates a given lines width with the new provided text size.
- */
- private float approximateLineWidth(Layout layout, int line, float textSize) {
- return layout.getLineWidth(line) * (textSize / layout.getPaint().getTextSize());
- }
- }
-
- private class SlidingTabStrip extends LinearLayout {
- private int mSelectedIndicatorHeight;
- private final Paint mSelectedIndicatorPaint;
-
- int mSelectedPosition = -1;
- float mSelectionOffset;
-
- private int mLayoutDirection = -1;
-
- private int mIndicatorLeft = -1;
- private int mIndicatorRight = -1;
-
- private ValueAnimator mIndicatorAnimator;
-
- SlidingTabStrip(Context context) {
- super(context);
- setWillNotDraw(false);
- mSelectedIndicatorPaint = new Paint();
- }
-
- void setSelectedIndicatorColor(int color) {
- if (mSelectedIndicatorPaint.getColor() != color) {
- mSelectedIndicatorPaint.setColor(color);
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- void setSelectedIndicatorHeight(int height) {
- if (mSelectedIndicatorHeight != height) {
- mSelectedIndicatorHeight = height;
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- boolean childrenNeedLayout() {
- for (int i = 0, z = getChildCount(); i < z; i++) {
- final View child = getChildAt(i);
- if (child.getWidth() <= 0) {
- return true;
- }
- }
- return false;
- }
-
- void setIndicatorPositionFromTabPosition(int position, float positionOffset) {
- if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
- mIndicatorAnimator.cancel();
- }
-
- mSelectedPosition = position;
- mSelectionOffset = positionOffset;
- updateIndicatorPosition();
- }
-
- float getIndicatorPosition() {
- return mSelectedPosition + mSelectionOffset;
- }
-
- @Override
- public void onRtlPropertiesChanged(int layoutDirection) {
- super.onRtlPropertiesChanged(layoutDirection);
-
- // Workaround for a bug before Android M where LinearLayout did not relayout itself when
- // layout direction changed.
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- //noinspection WrongConstant
- if (mLayoutDirection != layoutDirection) {
- requestLayout();
- mLayoutDirection = layoutDirection;
- }
- }
- }
-
- @Override
- protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
- // HorizontalScrollView will first measure use with UNSPECIFIED, and then with
- // EXACTLY. Ignore the first call since anything we do will be overwritten anyway
- return;
- }
-
- if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) {
- final int count = getChildCount();
-
- // First we'll find the widest tab
- int largestTabWidth = 0;
- for (int i = 0, z = count; i < z; i++) {
- View child = getChildAt(i);
- if (child.getVisibility() == VISIBLE) {
- largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth());
- }
- }
-
- if (largestTabWidth <= 0) {
- // If we don't have a largest child yet, skip until the next measure pass
- return;
- }
-
- final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN);
- boolean remeasure = false;
-
- if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) {
- // If the tabs fit within our width minus gutters, we will set all tabs to have
- // the same width
- for (int i = 0; i < count; i++) {
- final LinearLayout.LayoutParams lp =
- (LayoutParams) getChildAt(i).getLayoutParams();
- if (lp.width != largestTabWidth || lp.weight != 0) {
- lp.width = largestTabWidth;
- lp.weight = 0;
- remeasure = true;
- }
- }
- } else {
- // If the tabs will wrap to be larger than the width minus gutters, we need
- // to switch to GRAVITY_FILL
- mTabGravity = GRAVITY_FILL;
- updateTabViews(false);
- remeasure = true;
- }
-
- if (remeasure) {
- // Now re-measure after our changes
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
-
- if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
- // If we're currently running an animation, lets cancel it and start a
- // new animation with the remaining duration
- mIndicatorAnimator.cancel();
- final long duration = mIndicatorAnimator.getDuration();
- animateIndicatorToPosition(mSelectedPosition,
- Math.round((1f - mIndicatorAnimator.getAnimatedFraction()) * duration));
- } else {
- // If we've been layed out, update the indicator position
- updateIndicatorPosition();
- }
- }
-
- private void updateIndicatorPosition() {
- final View selectedTitle = getChildAt(mSelectedPosition);
- int left, right;
-
- if (selectedTitle != null && selectedTitle.getWidth() > 0) {
- left = selectedTitle.getLeft();
- right = selectedTitle.getRight();
-
- if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
- // Draw the selection partway between the tabs
- View nextTitle = getChildAt(mSelectedPosition + 1);
- left = (int) (mSelectionOffset * nextTitle.getLeft() +
- (1.0f - mSelectionOffset) * left);
- right = (int) (mSelectionOffset * nextTitle.getRight() +
- (1.0f - mSelectionOffset) * right);
- }
- } else {
- left = right = -1;
- }
-
- setIndicatorPosition(left, right);
- }
-
- void setIndicatorPosition(int left, int right) {
- if (left != mIndicatorLeft || right != mIndicatorRight) {
- // If the indicator's left/right has changed, invalidate
- mIndicatorLeft = left;
- mIndicatorRight = right;
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- void animateIndicatorToPosition(final int position, int duration) {
- if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
- mIndicatorAnimator.cancel();
- }
-
- final boolean isRtl = ViewCompat.getLayoutDirection(this)
- == ViewCompat.LAYOUT_DIRECTION_RTL;
-
- final View targetView = getChildAt(position);
- if (targetView == null) {
- // If we don't have a view, just update the position now and return
- updateIndicatorPosition();
- return;
- }
-
- final int targetLeft = targetView.getLeft();
- final int targetRight = targetView.getRight();
- final int startLeft;
- final int startRight;
-
- if (Math.abs(position - mSelectedPosition) <= 1) {
- // If the views are adjacent, we'll animate from edge-to-edge
- startLeft = mIndicatorLeft;
- startRight = mIndicatorRight;
- } else {
- // Else, we'll just grow from the nearest edge
- final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET);
- if (position < mSelectedPosition) {
- // We're going end-to-start
- if (isRtl) {
- startLeft = startRight = targetLeft - offset;
- } else {
- startLeft = startRight = targetRight + offset;
- }
- } else {
- // We're going start-to-end
- if (isRtl) {
- startLeft = startRight = targetRight + offset;
- } else {
- startLeft = startRight = targetLeft - offset;
- }
- }
- }
-
- if (startLeft != targetLeft || startRight != targetRight) {
- ValueAnimator animator = mIndicatorAnimator = new ValueAnimator();
- animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
- animator.setDuration(duration);
- animator.setFloatValues(0, 1);
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- final float fraction = animator.getAnimatedFraction();
- setIndicatorPosition(
- AnimationUtils.lerp(startLeft, targetLeft, fraction),
- AnimationUtils.lerp(startRight, targetRight, fraction));
- }
- });
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- mSelectedPosition = position;
- mSelectionOffset = 0f;
- }
- });
- animator.start();
- }
- }
-
- @Override
- public void draw(Canvas canvas) {
- super.draw(canvas);
-
- // Thick colored underline below the current selection
- if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
- canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
- mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
- }
- }
- }
-
- private static ColorStateList createColorStateList(int defaultColor, int selectedColor) {
- final int[][] states = new int[2][];
- final int[] colors = new int[2];
- int i = 0;
-
- states[i] = SELECTED_STATE_SET;
- colors[i] = selectedColor;
- i++;
-
- // Default enabled state
- states[i] = EMPTY_STATE_SET;
- colors[i] = defaultColor;
- i++;
-
- return new ColorStateList(states, colors);
- }
-
- private int getDefaultHeight() {
- boolean hasIconAndText = false;
- for (int i = 0, count = mTabs.size(); i < count; i++) {
- Tab tab = mTabs.get(i);
- if (tab != null && tab.getIcon() != null && !TextUtils.isEmpty(tab.getText())) {
- hasIconAndText = true;
- break;
- }
- }
- return hasIconAndText ? DEFAULT_HEIGHT_WITH_TEXT_ICON : DEFAULT_HEIGHT;
- }
-
- private int getTabMinWidth() {
- if (mRequestedTabMinWidth != INVALID_WIDTH) {
- // If we have been given a min width, use it
- return mRequestedTabMinWidth;
- }
- // Else, we'll use the default value
- return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0;
- }
-
- @Override
- public LayoutParams generateLayoutParams(AttributeSet attrs) {
- // We don't care about the layout params of any views added to us, since we don't actually
- // add them. The only view we add is the SlidingTabStrip, which is done manually.
- // We return the default layout params so that we don't blow up if we're given a TabItem
- // without android:layout_* values.
- return generateDefaultLayoutParams();
- }
-
- int getTabMaxWidth() {
- return mTabMaxWidth;
- }
-
- /**
- * A {@link ViewPager.OnPageChangeListener} class which contains the
- * necessary calls back to the provided {@link TabLayout} so that the tab position is
- * kept in sync.
- *
- * <p>This class stores the provided TabLayout weakly, meaning that you can use
- * {@link ViewPager#addOnPageChangeListener(ViewPager.OnPageChangeListener)
- * addOnPageChangeListener(OnPageChangeListener)} without removing the listener and
- * not cause a leak.
- */
- public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
- private final WeakReference<TabLayout> mTabLayoutRef;
- private int mPreviousScrollState;
- private int mScrollState;
-
- public TabLayoutOnPageChangeListener(TabLayout tabLayout) {
- mTabLayoutRef = new WeakReference<>(tabLayout);
- }
-
- @Override
- public void onPageScrollStateChanged(final int state) {
- mPreviousScrollState = mScrollState;
- mScrollState = state;
- }
-
- @Override
- public void onPageScrolled(final int position, final float positionOffset,
- final int positionOffsetPixels) {
- final TabLayout tabLayout = mTabLayoutRef.get();
- if (tabLayout != null) {
- // Only update the text selection if we're not settling, or we are settling after
- // being dragged
- final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
- mPreviousScrollState == SCROLL_STATE_DRAGGING;
- // Update the indicator if we're not settling after being idle. This is caused
- // from a setCurrentItem() call and will be handled by an animation from
- // onPageSelected() instead.
- final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING
- && mPreviousScrollState == SCROLL_STATE_IDLE);
- tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
- }
- }
-
- @Override
- public void onPageSelected(final int position) {
- final TabLayout tabLayout = mTabLayoutRef.get();
- if (tabLayout != null && tabLayout.getSelectedTabPosition() != position
- && position < tabLayout.getTabCount()) {
- // Select the tab, only updating the indicator if we're not being dragged/settled
- // (since onPageScrolled will handle that).
- final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE
- || (mScrollState == SCROLL_STATE_SETTLING
- && mPreviousScrollState == SCROLL_STATE_IDLE);
- tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator);
- }
- }
-
- void reset() {
- mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE;
- }
- }
-
- /**
- * A {@link TabLayout.OnTabSelectedListener} class which contains the necessary calls back
- * to the provided {@link ViewPager} so that the tab position is kept in sync.
- */
- public static class ViewPagerOnTabSelectedListener implements TabLayout.OnTabSelectedListener {
- private final ViewPager mViewPager;
-
- public ViewPagerOnTabSelectedListener(ViewPager viewPager) {
- mViewPager = viewPager;
- }
-
- @Override
- public void onTabSelected(TabLayout.Tab tab) {
- mViewPager.setCurrentItem(tab.getPosition());
- }
-
- @Override
- public void onTabUnselected(TabLayout.Tab tab) {
- // No-op
- }
-
- @Override
- public void onTabReselected(TabLayout.Tab tab) {
- // No-op
- }
- }
-
- private class PagerAdapterObserver extends DataSetObserver {
- PagerAdapterObserver() {
- }
-
- @Override
- public void onChanged() {
- populateFromPagerAdapter();
- }
-
- @Override
- public void onInvalidated() {
- populateFromPagerAdapter();
- }
- }
-
- private class AdapterChangeListener implements ViewPager.OnAdapterChangeListener {
- private boolean mAutoRefresh;
-
- AdapterChangeListener() {
- }
-
- @Override
- public void onAdapterChanged(@NonNull ViewPager viewPager,
- @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter) {
- if (mViewPager == viewPager) {
- setPagerAdapter(newAdapter, mAutoRefresh);
- }
- }
-
- void setAutoRefresh(boolean autoRefresh) {
- mAutoRefresh = autoRefresh;
- }
- }
-}
diff --git a/android/support/design/widget/TextInputEditText.java b/android/support/design/widget/TextInputEditText.java
deleted file mode 100644
index ee6c32c..0000000
--- a/android/support/design/widget/TextInputEditText.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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.support.design.widget;
-
-import android.content.Context;
-import android.support.v7.widget.AppCompatEditText;
-import android.support.v7.widget.WithHint;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewParent;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
-
-/**
- * A special sub-class of {@link android.widget.EditText} designed for use as a child of
- * {@link TextInputLayout}.
- *
- * <p>Using this class allows us to display a hint in the IME when in 'extract' mode.</p>
- */
-public class TextInputEditText extends AppCompatEditText {
-
- public TextInputEditText(Context context) {
- super(context);
- }
-
- public TextInputEditText(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public TextInputEditText(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- @Override
- public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
- final InputConnection ic = super.onCreateInputConnection(outAttrs);
- if (ic != null && outAttrs.hintText == null) {
- // If we don't have a hint and our parent implements WithHint, use its hint for the
- // EditorInfo. This allows us to display a hint in 'extract mode'.
- ViewParent parent = getParent();
- while (parent instanceof View) {
- if (parent instanceof WithHint) {
- outAttrs.hintText = ((WithHint) parent).getHint();
- break;
- }
- parent = parent.getParent();
- }
- }
- return ic;
- }
-}
diff --git a/android/support/design/widget/TextInputLayout.java b/android/support/design/widget/TextInputLayout.java
deleted file mode 100644
index 0540678..0000000
--- a/android/support/design/widget/TextInputLayout.java
+++ /dev/null
@@ -1,1530 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.Typeface;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.DrawableContainer;
-import android.os.Build;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.StringRes;
-import android.support.annotation.StyleRes;
-import android.support.annotation.VisibleForTesting;
-import android.support.design.R;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import android.support.v4.view.AbsSavedState;
-import android.support.v4.view.AccessibilityDelegateCompat;
-import android.support.v4.view.GravityCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
-import android.support.v4.widget.Space;
-import android.support.v4.widget.TextViewCompat;
-import android.support.v4.widget.ViewGroupUtils;
-import android.support.v7.content.res.AppCompatResources;
-import android.support.v7.widget.AppCompatDrawableManager;
-import android.support.v7.widget.AppCompatTextView;
-import android.support.v7.widget.TintTypedArray;
-import android.support.v7.widget.WithHint;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.text.method.PasswordTransformationMethod;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewStructure;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.animation.AccelerateInterpolator;
-import android.widget.EditText;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-/**
- * Layout which wraps an {@link android.widget.EditText} (or descendant) to show a floating label
- * when the hint is hidden due to the user inputting text.
- *
- * <p>Also supports showing an error via {@link #setErrorEnabled(boolean)} and
- * {@link #setError(CharSequence)}, and a character counter via
- * {@link #setCounterEnabled(boolean)}.</p>
- *
- * <p>Password visibility toggling is also supported via the
- * {@link #setPasswordVisibilityToggleEnabled(boolean)} API and related attribute.
- * If enabled, a button is displayed to toggle between the password being displayed as plain-text
- * or disguised, when your EditText is set to display a password.</p>
- *
- * <p><strong>Note:</strong> When using the password toggle functionality, the 'end' compound
- * drawable of the EditText will be overridden while the toggle is enabled. To ensure that any
- * existing drawables are restored correctly, you should set those compound drawables relatively
- * (start/end), opposed to absolutely (left/right).</p>
- *
- * The {@link TextInputEditText} class is provided to be used as a child of this layout. Using
- * TextInputEditText allows TextInputLayout greater control over the visual aspects of any
- * text input. An example usage is as so:
- *
- * <pre>
- * <android.support.design.widget.TextInputLayout
- * android:layout_width="match_parent"
- * android:layout_height="wrap_content">
- *
- * <android.support.design.widget.TextInputEditText
- * android:layout_width="match_parent"
- * android:layout_height="wrap_content"
- * android:hint="@string/form_username"/>
- *
- * </android.support.design.widget.TextInputLayout>
- * </pre>
- *
- * <p><strong>Note:</strong> The actual view hierarchy present under TextInputLayout is
- * <strong>NOT</strong> guaranteed to match the view hierarchy as written in XML. As a result,
- * calls to getParent() on children of the TextInputLayout -- such as an TextInputEditText --
- * may not return the TextInputLayout itself, but rather an intermediate View. If you need
- * to access a View directly, set an {@code android:id} and use {@link View#findViewById(int)}.
- */
-public class TextInputLayout extends LinearLayout implements WithHint {
-
- private static final int ANIMATION_DURATION = 200;
- private static final int INVALID_MAX_LENGTH = -1;
-
- private static final String LOG_TAG = "TextInputLayout";
-
- private final FrameLayout mInputFrame;
- EditText mEditText;
- private CharSequence mOriginalHint;
-
- private boolean mHintEnabled;
- private CharSequence mHint;
-
- private Paint mTmpPaint;
- private final Rect mTmpRect = new Rect();
-
- private LinearLayout mIndicatorArea;
- private int mIndicatorsAdded;
-
- private Typeface mTypeface;
-
- private boolean mErrorEnabled;
- TextView mErrorView;
- private int mErrorTextAppearance;
- private boolean mErrorShown;
- private CharSequence mError;
-
- boolean mCounterEnabled;
- private TextView mCounterView;
- private int mCounterMaxLength;
- private int mCounterTextAppearance;
- private int mCounterOverflowTextAppearance;
- private boolean mCounterOverflowed;
-
- private boolean mPasswordToggleEnabled;
- private Drawable mPasswordToggleDrawable;
- private CharSequence mPasswordToggleContentDesc;
- private CheckableImageButton mPasswordToggleView;
- private boolean mPasswordToggledVisible;
- private Drawable mPasswordToggleDummyDrawable;
- private Drawable mOriginalEditTextEndDrawable;
-
- private ColorStateList mPasswordToggleTintList;
- private boolean mHasPasswordToggleTintList;
- private PorterDuff.Mode mPasswordToggleTintMode;
- private boolean mHasPasswordToggleTintMode;
-
- private ColorStateList mDefaultTextColor;
- private ColorStateList mFocusedTextColor;
-
- // Only used for testing
- private boolean mHintExpanded;
-
- final CollapsingTextHelper mCollapsingTextHelper = new CollapsingTextHelper(this);
-
- private boolean mHintAnimationEnabled;
- private ValueAnimator mAnimator;
-
- private boolean mHasReconstructedEditTextBackground;
- private boolean mInDrawableStateChanged;
-
- private boolean mRestoringSavedState;
-
- public TextInputLayout(Context context) {
- this(context, null);
- }
-
- public TextInputLayout(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public TextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- // Can't call through to super(Context, AttributeSet, int) since it doesn't exist on API 10
- super(context, attrs);
-
- ThemeUtils.checkAppCompatTheme(context);
-
- setOrientation(VERTICAL);
- setWillNotDraw(false);
- setAddStatesFromChildren(true);
-
- mInputFrame = new FrameLayout(context);
- mInputFrame.setAddStatesFromChildren(true);
- addView(mInputFrame);
-
- mCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
- mCollapsingTextHelper.setPositionInterpolator(new AccelerateInterpolator());
- mCollapsingTextHelper.setCollapsedTextGravity(Gravity.TOP | GravityCompat.START);
-
- final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
- R.styleable.TextInputLayout, defStyleAttr, R.style.Widget_Design_TextInputLayout);
- mHintEnabled = a.getBoolean(R.styleable.TextInputLayout_hintEnabled, true);
- setHint(a.getText(R.styleable.TextInputLayout_android_hint));
- mHintAnimationEnabled = a.getBoolean(
- R.styleable.TextInputLayout_hintAnimationEnabled, true);
-
- if (a.hasValue(R.styleable.TextInputLayout_android_textColorHint)) {
- mDefaultTextColor = mFocusedTextColor =
- a.getColorStateList(R.styleable.TextInputLayout_android_textColorHint);
- }
-
- final int hintAppearance = a.getResourceId(
- R.styleable.TextInputLayout_hintTextAppearance, -1);
- if (hintAppearance != -1) {
- setHintTextAppearance(
- a.getResourceId(R.styleable.TextInputLayout_hintTextAppearance, 0));
- }
-
- mErrorTextAppearance = a.getResourceId(R.styleable.TextInputLayout_errorTextAppearance, 0);
- final boolean errorEnabled = a.getBoolean(R.styleable.TextInputLayout_errorEnabled, false);
-
- final boolean counterEnabled = a.getBoolean(
- R.styleable.TextInputLayout_counterEnabled, false);
- setCounterMaxLength(
- a.getInt(R.styleable.TextInputLayout_counterMaxLength, INVALID_MAX_LENGTH));
- mCounterTextAppearance = a.getResourceId(
- R.styleable.TextInputLayout_counterTextAppearance, 0);
- mCounterOverflowTextAppearance = a.getResourceId(
- R.styleable.TextInputLayout_counterOverflowTextAppearance, 0);
-
- mPasswordToggleEnabled = a.getBoolean(
- R.styleable.TextInputLayout_passwordToggleEnabled, false);
- mPasswordToggleDrawable = a.getDrawable(R.styleable.TextInputLayout_passwordToggleDrawable);
- mPasswordToggleContentDesc = a.getText(
- R.styleable.TextInputLayout_passwordToggleContentDescription);
- if (a.hasValue(R.styleable.TextInputLayout_passwordToggleTint)) {
- mHasPasswordToggleTintList = true;
- mPasswordToggleTintList = a.getColorStateList(
- R.styleable.TextInputLayout_passwordToggleTint);
- }
- if (a.hasValue(R.styleable.TextInputLayout_passwordToggleTintMode)) {
- mHasPasswordToggleTintMode = true;
- mPasswordToggleTintMode = ViewUtils.parseTintMode(
- a.getInt(R.styleable.TextInputLayout_passwordToggleTintMode, -1), null);
- }
-
- a.recycle();
-
- setErrorEnabled(errorEnabled);
- setCounterEnabled(counterEnabled);
- applyPasswordToggleTint();
-
- if (ViewCompat.getImportantForAccessibility(this)
- == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
- // Make sure we're important for accessibility if we haven't been explicitly not
- ViewCompat.setImportantForAccessibility(this,
- ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
- }
-
- ViewCompat.setAccessibilityDelegate(this, new TextInputAccessibilityDelegate());
- }
-
- @Override
- public void addView(View child, int index, final ViewGroup.LayoutParams params) {
- if (child instanceof EditText) {
- // Make sure that the EditText is vertically at the bottom, so that it sits on the
- // EditText's underline
- FrameLayout.LayoutParams flp = new FrameLayout.LayoutParams(params);
- flp.gravity = Gravity.CENTER_VERTICAL | (flp.gravity & ~Gravity.VERTICAL_GRAVITY_MASK);
- mInputFrame.addView(child, flp);
-
- // Now use the EditText's LayoutParams as our own and update them to make enough space
- // for the label
- mInputFrame.setLayoutParams(params);
- updateInputLayoutMargins();
-
- setEditText((EditText) child);
- } else {
- // Carry on adding the View...
- super.addView(child, index, params);
- }
- }
-
- /**
- * Set the typeface to use for the hint and any label views (such as counter and error views).
- *
- * @param typeface typeface to use, or {@code null} to use the default.
- */
- public void setTypeface(@Nullable Typeface typeface) {
- if ((mTypeface != null && !mTypeface.equals(typeface))
- || (mTypeface == null && typeface != null)) {
- mTypeface = typeface;
-
- mCollapsingTextHelper.setTypefaces(typeface);
- if (mCounterView != null) {
- mCounterView.setTypeface(typeface);
- }
- if (mErrorView != null) {
- mErrorView.setTypeface(typeface);
- }
- }
- }
-
- /**
- * Returns the typeface used for the hint and any label views (such as counter and error views).
- */
- @NonNull
- public Typeface getTypeface() {
- return mTypeface;
- }
-
- @Override
- public void dispatchProvideAutofillStructure(ViewStructure structure, int flags) {
- if (mOriginalHint == null || mEditText == null) {
- super.dispatchProvideAutofillStructure(structure, flags);
- return;
- }
-
- // Temporarily sets child's hint to its original value so it is properly set in the
- // child's ViewStructure.
- final CharSequence hint = mEditText.getHint();
- mEditText.setHint(mOriginalHint);
- try {
- super.dispatchProvideAutofillStructure(structure, flags);
- } finally {
- mEditText.setHint(hint);
- }
- }
-
- private void setEditText(EditText editText) {
- // If we already have an EditText, throw an exception
- if (mEditText != null) {
- throw new IllegalArgumentException("We already have an EditText, can only have one");
- }
-
- if (!(editText instanceof TextInputEditText)) {
- Log.i(LOG_TAG, "EditText added is not a TextInputEditText. Please switch to using that"
- + " class instead.");
- }
-
- mEditText = editText;
-
- final boolean hasPasswordTransformation = hasPasswordTransformation();
-
- // Use the EditText's typeface, and it's text size for our expanded text
- if (!hasPasswordTransformation) {
- // We don't want a monospace font just because we have a password field
- mCollapsingTextHelper.setTypefaces(mEditText.getTypeface());
- }
- mCollapsingTextHelper.setExpandedTextSize(mEditText.getTextSize());
-
- final int editTextGravity = mEditText.getGravity();
- mCollapsingTextHelper.setCollapsedTextGravity(
- Gravity.TOP | (editTextGravity & ~Gravity.VERTICAL_GRAVITY_MASK));
- mCollapsingTextHelper.setExpandedTextGravity(editTextGravity);
-
- // Add a TextWatcher so that we know when the text input has changed
- mEditText.addTextChangedListener(new TextWatcher() {
- @Override
- public void afterTextChanged(Editable s) {
- updateLabelState(!mRestoringSavedState);
- if (mCounterEnabled) {
- updateCounter(s.length());
- }
- }
-
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {}
- });
-
- // Use the EditText's hint colors if we don't have one set
- if (mDefaultTextColor == null) {
- mDefaultTextColor = mEditText.getHintTextColors();
- }
-
- // If we do not have a valid hint, try and retrieve it from the EditText, if enabled
- if (mHintEnabled && TextUtils.isEmpty(mHint)) {
- // Save the hint so it can be restored on dispatchProvideAutofillStructure();
- mOriginalHint = mEditText.getHint();
- setHint(mOriginalHint);
- // Clear the EditText's hint as we will display it ourselves
- mEditText.setHint(null);
- }
-
- if (mCounterView != null) {
- updateCounter(mEditText.getText().length());
- }
-
- if (mIndicatorArea != null) {
- adjustIndicatorPadding();
- }
-
- updatePasswordToggleView();
-
- // Update the label visibility with no animation, but force a state change
- updateLabelState(false, true);
- }
-
- private void updateInputLayoutMargins() {
- // Create/update the LayoutParams so that we can add enough top margin
- // to the EditText so make room for the label
- final LayoutParams lp = (LayoutParams) mInputFrame.getLayoutParams();
- final int newTopMargin;
-
- if (mHintEnabled) {
- if (mTmpPaint == null) {
- mTmpPaint = new Paint();
- }
- mTmpPaint.setTypeface(mCollapsingTextHelper.getCollapsedTypeface());
- mTmpPaint.setTextSize(mCollapsingTextHelper.getCollapsedTextSize());
- newTopMargin = (int) -mTmpPaint.ascent();
- } else {
- newTopMargin = 0;
- }
-
- if (newTopMargin != lp.topMargin) {
- lp.topMargin = newTopMargin;
- mInputFrame.requestLayout();
- }
- }
-
- void updateLabelState(boolean animate) {
- updateLabelState(animate, false);
- }
-
- void updateLabelState(final boolean animate, final boolean force) {
- final boolean isEnabled = isEnabled();
- final boolean hasText = mEditText != null && !TextUtils.isEmpty(mEditText.getText());
- final boolean isFocused = arrayContains(getDrawableState(), android.R.attr.state_focused);
- final boolean isErrorShowing = !TextUtils.isEmpty(getError());
-
- if (mDefaultTextColor != null) {
- mCollapsingTextHelper.setExpandedTextColor(mDefaultTextColor);
- }
-
- if (isEnabled && mCounterOverflowed && mCounterView != null) {
- mCollapsingTextHelper.setCollapsedTextColor(mCounterView.getTextColors());
- } else if (isEnabled && isFocused && mFocusedTextColor != null) {
- mCollapsingTextHelper.setCollapsedTextColor(mFocusedTextColor);
- } else if (mDefaultTextColor != null) {
- mCollapsingTextHelper.setCollapsedTextColor(mDefaultTextColor);
- }
-
- if (hasText || (isEnabled() && (isFocused || isErrorShowing))) {
- // We should be showing the label so do so if it isn't already
- if (force || mHintExpanded) {
- collapseHint(animate);
- }
- } else {
- // We should not be showing the label so hide it
- if (force || !mHintExpanded) {
- expandHint(animate);
- }
- }
- }
-
- /**
- * Returns the {@link android.widget.EditText} used for text input.
- */
- @Nullable
- public EditText getEditText() {
- return mEditText;
- }
-
- /**
- * Set the hint to be displayed in the floating label, if enabled.
- *
- * @see #setHintEnabled(boolean)
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_android_hint
- */
- public void setHint(@Nullable CharSequence hint) {
- if (mHintEnabled) {
- setHintInternal(hint);
- sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
- }
- }
-
- private void setHintInternal(CharSequence hint) {
- mHint = hint;
- mCollapsingTextHelper.setText(hint);
- }
-
- /**
- * Returns the hint which is displayed in the floating label, if enabled.
- *
- * @return the hint, or null if there isn't one set, or the hint is not enabled.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_android_hint
- */
- @Override
- @Nullable
- public CharSequence getHint() {
- return mHintEnabled ? mHint : null;
- }
-
- /**
- * Sets whether the floating label functionality is enabled or not in this layout.
- *
- * <p>If enabled, any non-empty hint in the child EditText will be moved into the floating
- * hint, and its existing hint will be cleared. If disabled, then any non-empty floating hint
- * in this layout will be moved into the EditText, and this layout's hint will be cleared.</p>
- *
- * @see #setHint(CharSequence)
- * @see #isHintEnabled()
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_hintEnabled
- */
- public void setHintEnabled(boolean enabled) {
- if (enabled != mHintEnabled) {
- mHintEnabled = enabled;
-
- final CharSequence editTextHint = mEditText.getHint();
- if (!mHintEnabled) {
- if (!TextUtils.isEmpty(mHint) && TextUtils.isEmpty(editTextHint)) {
- // If the hint is disabled, but we have a hint set, and the EditText doesn't,
- // pass it through...
- mEditText.setHint(mHint);
- }
- // Now clear out any set hint
- setHintInternal(null);
- } else {
- if (!TextUtils.isEmpty(editTextHint)) {
- // If the hint is now enabled and the EditText has one set, we'll use it if
- // we don't already have one, and clear the EditText's
- if (TextUtils.isEmpty(mHint)) {
- setHint(editTextHint);
- }
- mEditText.setHint(null);
- }
- }
-
- // Now update the EditText top margin
- if (mEditText != null) {
- updateInputLayoutMargins();
- }
- }
- }
-
- /**
- * Returns whether the floating label functionality is enabled or not in this layout.
- *
- * @see #setHintEnabled(boolean)
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_hintEnabled
- */
- public boolean isHintEnabled() {
- return mHintEnabled;
- }
-
- /**
- * Sets the hint text color, size, style from the specified TextAppearance resource.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_hintTextAppearance
- */
- public void setHintTextAppearance(@StyleRes int resId) {
- mCollapsingTextHelper.setCollapsedTextAppearance(resId);
- mFocusedTextColor = mCollapsingTextHelper.getCollapsedTextColor();
-
- if (mEditText != null) {
- updateLabelState(false);
- // Text size might have changed so update the top margin
- updateInputLayoutMargins();
- }
- }
-
- private void addIndicator(TextView indicator, int index) {
- if (mIndicatorArea == null) {
- mIndicatorArea = new LinearLayout(getContext());
- mIndicatorArea.setOrientation(LinearLayout.HORIZONTAL);
- addView(mIndicatorArea, LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT);
-
- // Add a flexible spacer in the middle so that the left/right views stay pinned
- final Space spacer = new Space(getContext());
- final LinearLayout.LayoutParams spacerLp = new LinearLayout.LayoutParams(0, 0, 1f);
- mIndicatorArea.addView(spacer, spacerLp);
-
- if (mEditText != null) {
- adjustIndicatorPadding();
- }
- }
- mIndicatorArea.setVisibility(View.VISIBLE);
- mIndicatorArea.addView(indicator, index);
- mIndicatorsAdded++;
- }
-
- private void adjustIndicatorPadding() {
- // Add padding to the error and character counter so that they match the EditText
- ViewCompat.setPaddingRelative(mIndicatorArea, ViewCompat.getPaddingStart(mEditText),
- 0, ViewCompat.getPaddingEnd(mEditText), mEditText.getPaddingBottom());
- }
-
- private void removeIndicator(TextView indicator) {
- if (mIndicatorArea != null) {
- mIndicatorArea.removeView(indicator);
- if (--mIndicatorsAdded == 0) {
- mIndicatorArea.setVisibility(View.GONE);
- }
- }
- }
-
- /**
- * Whether the error functionality is enabled or not in this layout. Enabling this
- * functionality before setting an error message via {@link #setError(CharSequence)}, will mean
- * that this layout will not change size when an error is displayed.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_errorEnabled
- */
- public void setErrorEnabled(boolean enabled) {
- if (mErrorEnabled != enabled) {
- if (mErrorView != null) {
- mErrorView.animate().cancel();
- }
-
- if (enabled) {
- mErrorView = new AppCompatTextView(getContext());
- mErrorView.setId(R.id.textinput_error);
- if (mTypeface != null) {
- mErrorView.setTypeface(mTypeface);
- }
- boolean useDefaultColor = false;
- try {
- TextViewCompat.setTextAppearance(mErrorView, mErrorTextAppearance);
-
- if (Build.VERSION.SDK_INT >= 23
- && mErrorView.getTextColors().getDefaultColor() == Color.MAGENTA) {
- // Caused by our theme not extending from Theme.Design*. On API 23 and
- // above, unresolved theme attrs result in MAGENTA rather than an exception.
- // Flag so that we use a decent default
- useDefaultColor = true;
- }
- } catch (Exception e) {
- // Caused by our theme not extending from Theme.Design*. Flag so that we use
- // a decent default
- useDefaultColor = true;
- }
- if (useDefaultColor) {
- // Probably caused by our theme not extending from Theme.Design*. Instead
- // we manually set something appropriate
- TextViewCompat.setTextAppearance(mErrorView,
- android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Caption);
- mErrorView.setTextColor(ContextCompat.getColor(getContext(),
- android.support.v7.appcompat.R.color.error_color_material));
- }
- mErrorView.setVisibility(INVISIBLE);
- ViewCompat.setAccessibilityLiveRegion(mErrorView,
- ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);
- addIndicator(mErrorView, 0);
- } else {
- mErrorShown = false;
- updateEditTextBackground();
- removeIndicator(mErrorView);
- mErrorView = null;
- }
- mErrorEnabled = enabled;
- }
- }
-
- /**
- * Sets the text color and size for the error message from the specified
- * TextAppearance resource.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_errorTextAppearance
- */
- public void setErrorTextAppearance(@StyleRes int resId) {
- mErrorTextAppearance = resId;
- if (mErrorView != null) {
- TextViewCompat.setTextAppearance(mErrorView, resId);
- }
- }
-
- /**
- * Returns whether the error functionality is enabled or not in this layout.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_errorEnabled
- *
- * @see #setErrorEnabled(boolean)
- */
- public boolean isErrorEnabled() {
- return mErrorEnabled;
- }
-
- /**
- * Sets an error message that will be displayed below our {@link EditText}. If the
- * {@code error} is {@code null}, the error message will be cleared.
- * <p>
- * If the error functionality has not been enabled via {@link #setErrorEnabled(boolean)}, then
- * it will be automatically enabled if {@code error} is not empty.
- *
- * @param error Error message to display, or null to clear
- *
- * @see #getError()
- */
- public void setError(@Nullable final CharSequence error) {
- // Only animate if we're enabled, laid out, and we have a different error message
- setError(error, ViewCompat.isLaidOut(this) && isEnabled()
- && (mErrorView == null || !TextUtils.equals(mErrorView.getText(), error)));
- }
-
- private void setError(@Nullable final CharSequence error, final boolean animate) {
- mError = error;
-
- if (!mErrorEnabled) {
- if (TextUtils.isEmpty(error)) {
- // If error isn't enabled, and the error is empty, just return
- return;
- }
- // Else, we'll assume that they want to enable the error functionality
- setErrorEnabled(true);
- }
-
- mErrorShown = !TextUtils.isEmpty(error);
-
- // Cancel any on-going animation
- mErrorView.animate().cancel();
-
- if (mErrorShown) {
- mErrorView.setText(error);
- mErrorView.setVisibility(VISIBLE);
-
- if (animate) {
- if (mErrorView.getAlpha() == 1f) {
- // If it's currently 100% show, we'll animate it from 0
- mErrorView.setAlpha(0f);
- }
- mErrorView.animate()
- .alpha(1f)
- .setDuration(ANIMATION_DURATION)
- .setInterpolator(AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR)
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animator) {
- mErrorView.setVisibility(VISIBLE);
- }
- }).start();
- } else {
- // Set alpha to 1f, just in case
- mErrorView.setAlpha(1f);
- }
- } else {
- if (mErrorView.getVisibility() == VISIBLE) {
- if (animate) {
- mErrorView.animate()
- .alpha(0f)
- .setDuration(ANIMATION_DURATION)
- .setInterpolator(AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR)
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- mErrorView.setText(error);
- mErrorView.setVisibility(INVISIBLE);
- }
- }).start();
- } else {
- mErrorView.setText(error);
- mErrorView.setVisibility(INVISIBLE);
- }
- }
- }
-
- updateEditTextBackground();
- updateLabelState(animate);
- }
-
- /**
- * Whether the character counter functionality is enabled or not in this layout.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_counterEnabled
- */
- public void setCounterEnabled(boolean enabled) {
- if (mCounterEnabled != enabled) {
- if (enabled) {
- mCounterView = new AppCompatTextView(getContext());
- mCounterView.setId(R.id.textinput_counter);
- if (mTypeface != null) {
- mCounterView.setTypeface(mTypeface);
- }
- mCounterView.setMaxLines(1);
- try {
- TextViewCompat.setTextAppearance(mCounterView, mCounterTextAppearance);
- } catch (Exception e) {
- // Probably caused by our theme not extending from Theme.Design*. Instead
- // we manually set something appropriate
- TextViewCompat.setTextAppearance(mCounterView,
- android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Caption);
- mCounterView.setTextColor(ContextCompat.getColor(getContext(),
- android.support.v7.appcompat.R.color.error_color_material));
- }
- addIndicator(mCounterView, -1);
- if (mEditText == null) {
- updateCounter(0);
- } else {
- updateCounter(mEditText.getText().length());
- }
- } else {
- removeIndicator(mCounterView);
- mCounterView = null;
- }
- mCounterEnabled = enabled;
- }
- }
-
- /**
- * Returns whether the character counter functionality is enabled or not in this layout.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_counterEnabled
- *
- * @see #setCounterEnabled(boolean)
- */
- public boolean isCounterEnabled() {
- return mCounterEnabled;
- }
-
- /**
- * Sets the max length to display at the character counter.
- *
- * @param maxLength maxLength to display. Any value less than or equal to 0 will not be shown.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_counterMaxLength
- */
- public void setCounterMaxLength(int maxLength) {
- if (mCounterMaxLength != maxLength) {
- if (maxLength > 0) {
- mCounterMaxLength = maxLength;
- } else {
- mCounterMaxLength = INVALID_MAX_LENGTH;
- }
- if (mCounterEnabled) {
- updateCounter(mEditText == null ? 0 : mEditText.getText().length());
- }
- }
- }
-
- @Override
- public void setEnabled(boolean enabled) {
- // Since we're set to addStatesFromChildren, we need to make sure that we set all
- // children to enabled/disabled otherwise any enabled children will wipe out our disabled
- // drawable state
- recursiveSetEnabled(this, enabled);
- super.setEnabled(enabled);
- }
-
- private static void recursiveSetEnabled(final ViewGroup vg, final boolean enabled) {
- for (int i = 0, count = vg.getChildCount(); i < count; i++) {
- final View child = vg.getChildAt(i);
- child.setEnabled(enabled);
- if (child instanceof ViewGroup) {
- recursiveSetEnabled((ViewGroup) child, enabled);
- }
- }
- }
-
- /**
- * Returns the max length shown at the character counter.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_counterMaxLength
- */
- public int getCounterMaxLength() {
- return mCounterMaxLength;
- }
-
- void updateCounter(int length) {
- boolean wasCounterOverflowed = mCounterOverflowed;
- if (mCounterMaxLength == INVALID_MAX_LENGTH) {
- mCounterView.setText(String.valueOf(length));
- mCounterOverflowed = false;
- } else {
- mCounterOverflowed = length > mCounterMaxLength;
- if (wasCounterOverflowed != mCounterOverflowed) {
- TextViewCompat.setTextAppearance(mCounterView, mCounterOverflowed
- ? mCounterOverflowTextAppearance : mCounterTextAppearance);
- }
- mCounterView.setText(getContext().getString(R.string.character_counter_pattern,
- length, mCounterMaxLength));
- }
- if (mEditText != null && wasCounterOverflowed != mCounterOverflowed) {
- updateLabelState(false);
- updateEditTextBackground();
- }
- }
-
- private void updateEditTextBackground() {
- if (mEditText == null) {
- return;
- }
-
- Drawable editTextBackground = mEditText.getBackground();
- if (editTextBackground == null) {
- return;
- }
-
- ensureBackgroundDrawableStateWorkaround();
-
- if (android.support.v7.widget.DrawableUtils.canSafelyMutateDrawable(editTextBackground)) {
- editTextBackground = editTextBackground.mutate();
- }
-
- if (mErrorShown && mErrorView != null) {
- // Set a color filter of the error color
- editTextBackground.setColorFilter(
- AppCompatDrawableManager.getPorterDuffColorFilter(
- mErrorView.getCurrentTextColor(), PorterDuff.Mode.SRC_IN));
- } else if (mCounterOverflowed && mCounterView != null) {
- // Set a color filter of the counter color
- editTextBackground.setColorFilter(
- AppCompatDrawableManager.getPorterDuffColorFilter(
- mCounterView.getCurrentTextColor(), PorterDuff.Mode.SRC_IN));
- } else {
- // Else reset the color filter and refresh the drawable state so that the
- // normal tint is used
- DrawableCompat.clearColorFilter(editTextBackground);
- mEditText.refreshDrawableState();
- }
- }
-
- private void ensureBackgroundDrawableStateWorkaround() {
- final int sdk = Build.VERSION.SDK_INT;
- if (sdk != 21 && sdk != 22) {
- // The workaround is only required on API 21-22
- return;
- }
- final Drawable bg = mEditText.getBackground();
- if (bg == null) {
- return;
- }
-
- if (!mHasReconstructedEditTextBackground) {
- // This is gross. There is an issue in the platform which affects container Drawables
- // where the first drawable retrieved from resources will propagate any changes
- // (like color filter) to all instances from the cache. We'll try to workaround it...
-
- final Drawable newBg = bg.getConstantState().newDrawable();
-
- if (bg instanceof DrawableContainer) {
- // If we have a Drawable container, we can try and set it's constant state via
- // reflection from the new Drawable
- mHasReconstructedEditTextBackground =
- DrawableUtils.setContainerConstantState(
- (DrawableContainer) bg, newBg.getConstantState());
- }
-
- if (!mHasReconstructedEditTextBackground) {
- // If we reach here then we just need to set a brand new instance of the Drawable
- // as the background. This has the unfortunate side-effect of wiping out any
- // user set padding, but I'd hope that use of custom padding on an EditText
- // is limited.
- ViewCompat.setBackground(mEditText, newBg);
- mHasReconstructedEditTextBackground = true;
- }
- }
- }
-
- static class SavedState extends AbsSavedState {
- CharSequence error;
- boolean isPasswordToggledVisible;
-
- SavedState(Parcelable superState) {
- super(superState);
- }
-
- SavedState(Parcel source, ClassLoader loader) {
- super(source, loader);
- error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
- isPasswordToggledVisible = (source.readInt() == 1);
-
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- super.writeToParcel(dest, flags);
- TextUtils.writeToParcel(error, dest, flags);
- dest.writeInt(isPasswordToggledVisible ? 1 : 0);
- }
-
- @Override
- public String toString() {
- return "TextInputLayout.SavedState{"
- + Integer.toHexString(System.identityHashCode(this))
- + " error=" + error + "}";
- }
-
- public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in, ClassLoader loader) {
- return new SavedState(in, loader);
- }
-
- @Override
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in, null);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
-
- @Override
- public Parcelable onSaveInstanceState() {
- Parcelable superState = super.onSaveInstanceState();
- SavedState ss = new SavedState(superState);
- if (mErrorShown) {
- ss.error = getError();
- }
- ss.isPasswordToggledVisible = mPasswordToggledVisible;
- return ss;
- }
-
- @Override
- protected void onRestoreInstanceState(Parcelable state) {
- if (!(state instanceof SavedState)) {
- super.onRestoreInstanceState(state);
- return;
- }
- SavedState ss = (SavedState) state;
- super.onRestoreInstanceState(ss.getSuperState());
- setError(ss.error);
- if (ss.isPasswordToggledVisible) {
- passwordVisibilityToggleRequested(true);
- }
- requestLayout();
- }
-
- @Override
- protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
- mRestoringSavedState = true;
- super.dispatchRestoreInstanceState(container);
- mRestoringSavedState = false;
- }
-
- /**
- * Returns the error message that was set to be displayed with
- * {@link #setError(CharSequence)}, or <code>null</code> if no error was set
- * or if error displaying is not enabled.
- *
- * @see #setError(CharSequence)
- */
- @Nullable
- public CharSequence getError() {
- return mErrorEnabled ? mError : null;
- }
-
- /**
- * Returns whether any hint state changes, due to being focused or non-empty text, are
- * animated.
- *
- * @see #setHintAnimationEnabled(boolean)
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_hintAnimationEnabled
- */
- public boolean isHintAnimationEnabled() {
- return mHintAnimationEnabled;
- }
-
- /**
- * Set whether any hint state changes, due to being focused or non-empty text, are
- * animated.
- *
- * @see #isHintAnimationEnabled()
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_hintAnimationEnabled
- */
- public void setHintAnimationEnabled(boolean enabled) {
- mHintAnimationEnabled = enabled;
- }
-
- @Override
- public void draw(Canvas canvas) {
- super.draw(canvas);
-
- if (mHintEnabled) {
- mCollapsingTextHelper.draw(canvas);
- }
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- updatePasswordToggleView();
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- private void updatePasswordToggleView() {
- if (mEditText == null) {
- // If there is no EditText, there is nothing to update
- return;
- }
-
- if (shouldShowPasswordIcon()) {
- if (mPasswordToggleView == null) {
- mPasswordToggleView = (CheckableImageButton) LayoutInflater.from(getContext())
- .inflate(R.layout.design_text_input_password_icon, mInputFrame, false);
- mPasswordToggleView.setImageDrawable(mPasswordToggleDrawable);
- mPasswordToggleView.setContentDescription(mPasswordToggleContentDesc);
- mInputFrame.addView(mPasswordToggleView);
-
- mPasswordToggleView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- passwordVisibilityToggleRequested(false);
- }
- });
- }
-
- if (mEditText != null && ViewCompat.getMinimumHeight(mEditText) <= 0) {
- // We should make sure that the EditText has the same min-height as the password
- // toggle view. This ensure focus works properly, and there is no visual jump
- // if the password toggle is enabled/disabled.
- mEditText.setMinimumHeight(ViewCompat.getMinimumHeight(mPasswordToggleView));
- }
-
- mPasswordToggleView.setVisibility(VISIBLE);
- mPasswordToggleView.setChecked(mPasswordToggledVisible);
-
- // We need to add a dummy drawable as the end compound drawable so that the text is
- // indented and doesn't display below the toggle view
- if (mPasswordToggleDummyDrawable == null) {
- mPasswordToggleDummyDrawable = new ColorDrawable();
- }
- mPasswordToggleDummyDrawable.setBounds(0, 0, mPasswordToggleView.getMeasuredWidth(), 1);
-
- final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(mEditText);
- // Store the user defined end compound drawable so that we can restore it later
- if (compounds[2] != mPasswordToggleDummyDrawable) {
- mOriginalEditTextEndDrawable = compounds[2];
- }
- TextViewCompat.setCompoundDrawablesRelative(mEditText, compounds[0], compounds[1],
- mPasswordToggleDummyDrawable, compounds[3]);
-
- // Copy over the EditText's padding so that we match
- mPasswordToggleView.setPadding(mEditText.getPaddingLeft(),
- mEditText.getPaddingTop(), mEditText.getPaddingRight(),
- mEditText.getPaddingBottom());
- } else {
- if (mPasswordToggleView != null && mPasswordToggleView.getVisibility() == VISIBLE) {
- mPasswordToggleView.setVisibility(View.GONE);
- }
-
- if (mPasswordToggleDummyDrawable != null) {
- // Make sure that we remove the dummy end compound drawable if it exists, and then
- // clear it
- final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(mEditText);
- if (compounds[2] == mPasswordToggleDummyDrawable) {
- TextViewCompat.setCompoundDrawablesRelative(mEditText, compounds[0],
- compounds[1], mOriginalEditTextEndDrawable, compounds[3]);
- mPasswordToggleDummyDrawable = null;
- }
- }
- }
- }
-
- /**
- * Set the icon to use for the password visibility toggle button.
- *
- * <p>If you use an icon you should also set a description for its action
- * using {@link #setPasswordVisibilityToggleContentDescription(CharSequence)}.
- * This is used for accessibility.</p>
- *
- * @param resId resource id of the drawable to set, or 0 to clear the icon
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleDrawable
- */
- public void setPasswordVisibilityToggleDrawable(@DrawableRes int resId) {
- setPasswordVisibilityToggleDrawable(resId != 0
- ? AppCompatResources.getDrawable(getContext(), resId)
- : null);
- }
-
- /**
- * Set the icon to use for the password visibility toggle button.
- *
- * <p>If you use an icon you should also set a description for its action
- * using {@link #setPasswordVisibilityToggleContentDescription(CharSequence)}.
- * This is used for accessibility.</p>
- *
- * @param icon Drawable to set, may be null to clear the icon
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleDrawable
- */
- public void setPasswordVisibilityToggleDrawable(@Nullable Drawable icon) {
- mPasswordToggleDrawable = icon;
- if (mPasswordToggleView != null) {
- mPasswordToggleView.setImageDrawable(icon);
- }
- }
-
- /**
- * Set a content description for the navigation button if one is present.
- *
- * <p>The content description will be read via screen readers or other accessibility
- * systems to explain the action of the password visibility toggle.</p>
- *
- * @param resId Resource ID of a content description string to set,
- * or 0 to clear the description
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleContentDescription
- */
- public void setPasswordVisibilityToggleContentDescription(@StringRes int resId) {
- setPasswordVisibilityToggleContentDescription(
- resId != 0 ? getResources().getText(resId) : null);
- }
-
- /**
- * Set a content description for the navigation button if one is present.
- *
- * <p>The content description will be read via screen readers or other accessibility
- * systems to explain the action of the password visibility toggle.</p>
- *
- * @param description Content description to set, or null to clear the content description
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleContentDescription
- */
- public void setPasswordVisibilityToggleContentDescription(@Nullable CharSequence description) {
- mPasswordToggleContentDesc = description;
- if (mPasswordToggleView != null) {
- mPasswordToggleView.setContentDescription(description);
- }
- }
-
- /**
- * Returns the icon currently used for the password visibility toggle button.
- *
- * @see #setPasswordVisibilityToggleDrawable(Drawable)
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleDrawable
- */
- @Nullable
- public Drawable getPasswordVisibilityToggleDrawable() {
- return mPasswordToggleDrawable;
- }
-
- /**
- * Returns the currently configured content description for the password visibility
- * toggle button.
- *
- * <p>This will be used to describe the navigation action to users through mechanisms
- * such as screen readers.</p>
- */
- @Nullable
- public CharSequence getPasswordVisibilityToggleContentDescription() {
- return mPasswordToggleContentDesc;
- }
-
- /**
- * Returns whether the password visibility toggle functionality is currently enabled.
- *
- * @see #setPasswordVisibilityToggleEnabled(boolean)
- */
- public boolean isPasswordVisibilityToggleEnabled() {
- return mPasswordToggleEnabled;
- }
-
- /**
- * Returns whether the password visibility toggle functionality is enabled or not.
- *
- * <p>When enabled, a button is placed at the end of the EditText which enables the user
- * to switch between the field's input being visibly disguised or not.</p>
- *
- * @param enabled true to enable the functionality
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleEnabled
- */
- public void setPasswordVisibilityToggleEnabled(final boolean enabled) {
- if (mPasswordToggleEnabled != enabled) {
- mPasswordToggleEnabled = enabled;
-
- if (!enabled && mPasswordToggledVisible && mEditText != null) {
- // If the toggle is no longer enabled, but we remove the PasswordTransformation
- // to make the password visible, add it back
- mEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
- }
-
- // Reset the visibility tracking flag
- mPasswordToggledVisible = false;
-
- updatePasswordToggleView();
- }
- }
-
- /**
- * Applies a tint to the the password visibility toggle drawable. Does not modify the current
- * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
- *
- * <p>Subsequent calls to {@link #setPasswordVisibilityToggleDrawable(Drawable)} will
- * automatically mutate the drawable and apply the specified tint and tint mode using
- * {@link DrawableCompat#setTintList(Drawable, ColorStateList)}.</p>
- *
- * @param tintList the tint to apply, may be null to clear tint
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleTint
- */
- public void setPasswordVisibilityToggleTintList(@Nullable ColorStateList tintList) {
- mPasswordToggleTintList = tintList;
- mHasPasswordToggleTintList = true;
- applyPasswordToggleTint();
- }
-
- /**
- * Specifies the blending mode used to apply the tint specified by
- * {@link #setPasswordVisibilityToggleTintList(ColorStateList)} to the password
- * visibility toggle drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.</p>
- *
- * @param mode the blending mode used to apply the tint, may be null to clear tint
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleTintMode
- */
- public void setPasswordVisibilityToggleTintMode(@Nullable PorterDuff.Mode mode) {
- mPasswordToggleTintMode = mode;
- mHasPasswordToggleTintMode = true;
- applyPasswordToggleTint();
- }
-
- private void passwordVisibilityToggleRequested(boolean shouldSkipAnimations) {
- if (mPasswordToggleEnabled) {
- // Store the current cursor position
- final int selection = mEditText.getSelectionEnd();
-
- if (hasPasswordTransformation()) {
- mEditText.setTransformationMethod(null);
- mPasswordToggledVisible = true;
- } else {
- mEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
- mPasswordToggledVisible = false;
- }
-
- mPasswordToggleView.setChecked(mPasswordToggledVisible);
- if (shouldSkipAnimations) {
- mPasswordToggleView.jumpDrawablesToCurrentState();
- }
-
- // And restore the cursor position
- mEditText.setSelection(selection);
- }
- }
-
- private boolean hasPasswordTransformation() {
- return mEditText != null
- && mEditText.getTransformationMethod() instanceof PasswordTransformationMethod;
- }
-
- private boolean shouldShowPasswordIcon() {
- return mPasswordToggleEnabled && (hasPasswordTransformation() || mPasswordToggledVisible);
- }
-
- private void applyPasswordToggleTint() {
- if (mPasswordToggleDrawable != null
- && (mHasPasswordToggleTintList || mHasPasswordToggleTintMode)) {
- mPasswordToggleDrawable = DrawableCompat.wrap(mPasswordToggleDrawable).mutate();
-
- if (mHasPasswordToggleTintList) {
- DrawableCompat.setTintList(mPasswordToggleDrawable, mPasswordToggleTintList);
- }
- if (mHasPasswordToggleTintMode) {
- DrawableCompat.setTintMode(mPasswordToggleDrawable, mPasswordToggleTintMode);
- }
-
- if (mPasswordToggleView != null
- && mPasswordToggleView.getDrawable() != mPasswordToggleDrawable) {
- mPasswordToggleView.setImageDrawable(mPasswordToggleDrawable);
- }
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
-
- if (mHintEnabled && mEditText != null) {
- final Rect rect = mTmpRect;
- ViewGroupUtils.getDescendantRect(this, mEditText, rect);
-
- final int l = rect.left + mEditText.getCompoundPaddingLeft();
- final int r = rect.right - mEditText.getCompoundPaddingRight();
-
- mCollapsingTextHelper.setExpandedBounds(
- l, rect.top + mEditText.getCompoundPaddingTop(),
- r, rect.bottom - mEditText.getCompoundPaddingBottom());
-
- // Set the collapsed bounds to be the the full height (minus padding) to match the
- // EditText's editable area
- mCollapsingTextHelper.setCollapsedBounds(l, getPaddingTop(),
- r, bottom - top - getPaddingBottom());
-
- mCollapsingTextHelper.recalculate();
- }
- }
-
- private void collapseHint(boolean animate) {
- if (mAnimator != null && mAnimator.isRunning()) {
- mAnimator.cancel();
- }
- if (animate && mHintAnimationEnabled) {
- animateToExpansionFraction(1f);
- } else {
- mCollapsingTextHelper.setExpansionFraction(1f);
- }
- mHintExpanded = false;
- }
-
- @Override
- protected void drawableStateChanged() {
- if (mInDrawableStateChanged) {
- // Some of the calls below will update the drawable state of child views. Since we're
- // using addStatesFromChildren we can get into infinite recursion, hence we'll just
- // exit in this instance
- return;
- }
-
- mInDrawableStateChanged = true;
-
- super.drawableStateChanged();
-
- final int[] state = getDrawableState();
- boolean changed = false;
-
- // Drawable state has changed so see if we need to update the label
- updateLabelState(ViewCompat.isLaidOut(this) && isEnabled());
-
- updateEditTextBackground();
-
- if (mCollapsingTextHelper != null) {
- changed |= mCollapsingTextHelper.setState(state);
- }
-
- if (changed) {
- invalidate();
- }
-
- mInDrawableStateChanged = false;
- }
-
- private void expandHint(boolean animate) {
- if (mAnimator != null && mAnimator.isRunning()) {
- mAnimator.cancel();
- }
- if (animate && mHintAnimationEnabled) {
- animateToExpansionFraction(0f);
- } else {
- mCollapsingTextHelper.setExpansionFraction(0f);
- }
- mHintExpanded = true;
- }
-
- @VisibleForTesting
- void animateToExpansionFraction(final float target) {
- if (mCollapsingTextHelper.getExpansionFraction() == target) {
- return;
- }
- if (mAnimator == null) {
- mAnimator = new ValueAnimator();
- mAnimator.setInterpolator(AnimationUtils.LINEAR_INTERPOLATOR);
- mAnimator.setDuration(ANIMATION_DURATION);
- mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- mCollapsingTextHelper.setExpansionFraction((float) animator.getAnimatedValue());
- }
- });
- }
- mAnimator.setFloatValues(mCollapsingTextHelper.getExpansionFraction(), target);
- mAnimator.start();
- }
-
- @VisibleForTesting
- final boolean isHintExpanded() {
- return mHintExpanded;
- }
-
- private class TextInputAccessibilityDelegate extends AccessibilityDelegateCompat {
- TextInputAccessibilityDelegate() {
- }
-
- @Override
- public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
- super.onInitializeAccessibilityEvent(host, event);
- event.setClassName(TextInputLayout.class.getSimpleName());
- }
-
- @Override
- public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
- super.onPopulateAccessibilityEvent(host, event);
-
- final CharSequence text = mCollapsingTextHelper.getText();
- if (!TextUtils.isEmpty(text)) {
- event.getText().add(text);
- }
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
- super.onInitializeAccessibilityNodeInfo(host, info);
- info.setClassName(TextInputLayout.class.getSimpleName());
-
- final CharSequence text = mCollapsingTextHelper.getText();
- if (!TextUtils.isEmpty(text)) {
- info.setText(text);
- }
- if (mEditText != null) {
- info.setLabelFor(mEditText);
- }
- final CharSequence error = mErrorView != null ? mErrorView.getText() : null;
- if (!TextUtils.isEmpty(error)) {
- info.setContentInvalid(true);
- info.setError(error);
- }
- }
- }
-
- private static boolean arrayContains(int[] array, int value) {
- for (int v : array) {
- if (v == value) {
- return true;
- }
- }
- return false;
- }
-}
diff --git a/android/support/design/widget/ThemeUtils.java b/android/support/design/widget/ThemeUtils.java
deleted file mode 100644
index 821dcb6..0000000
--- a/android/support/design/widget/ThemeUtils.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-
-class ThemeUtils {
-
- private static final int[] APPCOMPAT_CHECK_ATTRS = {
- android.support.v7.appcompat.R.attr.colorPrimary
- };
-
- static void checkAppCompatTheme(Context context) {
- TypedArray a = context.obtainStyledAttributes(APPCOMPAT_CHECK_ATTRS);
- final boolean failed = !a.hasValue(0);
- a.recycle();
- if (failed) {
- throw new IllegalArgumentException("You need to use a Theme.AppCompat theme "
- + "(or descendant) with the design library.");
- }
- }
-}
diff --git a/android/support/design/widget/ViewOffsetBehavior.java b/android/support/design/widget/ViewOffsetBehavior.java
deleted file mode 100644
index 541de69..0000000
--- a/android/support/design/widget/ViewOffsetBehavior.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-
-/**
- * Behavior will automatically sets up a {@link ViewOffsetHelper} on a {@link View}.
- */
-class ViewOffsetBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
-
- private ViewOffsetHelper mViewOffsetHelper;
-
- private int mTempTopBottomOffset = 0;
- private int mTempLeftRightOffset = 0;
-
- public ViewOffsetBehavior() {}
-
- public ViewOffsetBehavior(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
- // First let lay the child out
- layoutChild(parent, child, layoutDirection);
-
- if (mViewOffsetHelper == null) {
- mViewOffsetHelper = new ViewOffsetHelper(child);
- }
- mViewOffsetHelper.onViewLayout();
-
- if (mTempTopBottomOffset != 0) {
- mViewOffsetHelper.setTopAndBottomOffset(mTempTopBottomOffset);
- mTempTopBottomOffset = 0;
- }
- if (mTempLeftRightOffset != 0) {
- mViewOffsetHelper.setLeftAndRightOffset(mTempLeftRightOffset);
- mTempLeftRightOffset = 0;
- }
-
- return true;
- }
-
- protected void layoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
- // Let the parent lay it out by default
- parent.onLayoutChild(child, layoutDirection);
- }
-
- public boolean setTopAndBottomOffset(int offset) {
- if (mViewOffsetHelper != null) {
- return mViewOffsetHelper.setTopAndBottomOffset(offset);
- } else {
- mTempTopBottomOffset = offset;
- }
- return false;
- }
-
- public boolean setLeftAndRightOffset(int offset) {
- if (mViewOffsetHelper != null) {
- return mViewOffsetHelper.setLeftAndRightOffset(offset);
- } else {
- mTempLeftRightOffset = offset;
- }
- return false;
- }
-
- public int getTopAndBottomOffset() {
- return mViewOffsetHelper != null ? mViewOffsetHelper.getTopAndBottomOffset() : 0;
- }
-
- public int getLeftAndRightOffset() {
- return mViewOffsetHelper != null ? mViewOffsetHelper.getLeftAndRightOffset() : 0;
- }
-}
\ No newline at end of file
diff --git a/android/support/design/widget/ViewOffsetHelper.java b/android/support/design/widget/ViewOffsetHelper.java
deleted file mode 100644
index 088430a..0000000
--- a/android/support/design/widget/ViewOffsetHelper.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.support.v4.view.ViewCompat;
-import android.view.View;
-
-/**
- * Utility helper for moving a {@link android.view.View} around using
- * {@link android.view.View#offsetLeftAndRight(int)} and
- * {@link android.view.View#offsetTopAndBottom(int)}.
- * <p>
- * Also the setting of absolute offsets (similar to translationX/Y), rather than additive
- * offsets.
- */
-class ViewOffsetHelper {
-
- private final View mView;
-
- private int mLayoutTop;
- private int mLayoutLeft;
- private int mOffsetTop;
- private int mOffsetLeft;
-
- public ViewOffsetHelper(View view) {
- mView = view;
- }
-
- public void onViewLayout() {
- // Now grab the intended top
- mLayoutTop = mView.getTop();
- mLayoutLeft = mView.getLeft();
-
- // And offset it as needed
- updateOffsets();
- }
-
- private void updateOffsets() {
- ViewCompat.offsetTopAndBottom(mView, mOffsetTop - (mView.getTop() - mLayoutTop));
- ViewCompat.offsetLeftAndRight(mView, mOffsetLeft - (mView.getLeft() - mLayoutLeft));
- }
-
- /**
- * Set the top and bottom offset for this {@link ViewOffsetHelper}'s view.
- *
- * @param offset the offset in px.
- * @return true if the offset has changed
- */
- public boolean setTopAndBottomOffset(int offset) {
- if (mOffsetTop != offset) {
- mOffsetTop = offset;
- updateOffsets();
- return true;
- }
- return false;
- }
-
- /**
- * Set the left and right offset for this {@link ViewOffsetHelper}'s view.
- *
- * @param offset the offset in px.
- * @return true if the offset has changed
- */
- public boolean setLeftAndRightOffset(int offset) {
- if (mOffsetLeft != offset) {
- mOffsetLeft = offset;
- updateOffsets();
- return true;
- }
- return false;
- }
-
- public int getTopAndBottomOffset() {
- return mOffsetTop;
- }
-
- public int getLeftAndRightOffset() {
- return mOffsetLeft;
- }
-
- public int getLayoutTop() {
- return mLayoutTop;
- }
-
- public int getLayoutLeft() {
- return mLayoutLeft;
- }
-}
\ No newline at end of file
diff --git a/android/support/design/widget/ViewUtils.java b/android/support/design/widget/ViewUtils.java
deleted file mode 100644
index c09eac1..0000000
--- a/android/support/design/widget/ViewUtils.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.graphics.PorterDuff;
-
-class ViewUtils {
- static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) {
- switch (value) {
- case 3:
- return PorterDuff.Mode.SRC_OVER;
- case 5:
- return PorterDuff.Mode.SRC_IN;
- case 9:
- return PorterDuff.Mode.SRC_ATOP;
- case 14:
- return PorterDuff.Mode.MULTIPLY;
- case 15:
- return PorterDuff.Mode.SCREEN;
- default:
- return defaultMode;
- }
- }
-
-}
diff --git a/android/support/design/widget/ViewUtilsLollipop.java b/android/support/design/widget/ViewUtilsLollipop.java
deleted file mode 100644
index 5927e9b..0000000
--- a/android/support/design/widget/ViewUtilsLollipop.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.animation.AnimatorInflater;
-import android.animation.ObjectAnimator;
-import android.animation.StateListAnimator;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.support.annotation.RequiresApi;
-import android.support.design.R;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewOutlineProvider;
-
-@RequiresApi(21)
-class ViewUtilsLollipop {
-
- private static final int[] STATE_LIST_ANIM_ATTRS = new int[] {android.R.attr.stateListAnimator};
-
- static void setBoundsViewOutlineProvider(View view) {
- view.setOutlineProvider(ViewOutlineProvider.BOUNDS);
- }
-
- static void setStateListAnimatorFromAttrs(View view, AttributeSet attrs,
- int defStyleAttr, int defStyleRes) {
- final Context context = view.getContext();
- final TypedArray a = context.obtainStyledAttributes(attrs, STATE_LIST_ANIM_ATTRS,
- defStyleAttr, defStyleRes);
- try {
- if (a.hasValue(0)) {
- StateListAnimator sla = AnimatorInflater.loadStateListAnimator(context,
- a.getResourceId(0, 0));
- view.setStateListAnimator(sla);
- }
- } finally {
- a.recycle();
- }
- }
-
- /**
- * Creates and sets a {@link StateListAnimator} with a custom elevation value
- */
- static void setDefaultAppBarLayoutStateListAnimator(final View view, final float elevation) {
- final int dur = view.getResources().getInteger(R.integer.app_bar_elevation_anim_duration);
-
- final StateListAnimator sla = new StateListAnimator();
-
- // Enabled and collapsible, but not collapsed means not elevated
- sla.addState(new int[]{android.R.attr.enabled, R.attr.state_collapsible,
- -R.attr.state_collapsed},
- ObjectAnimator.ofFloat(view, "elevation", 0f).setDuration(dur));
-
- // Default enabled state
- sla.addState(new int[]{android.R.attr.enabled},
- ObjectAnimator.ofFloat(view, "elevation", elevation).setDuration(dur));
-
- // Disabled state
- sla.addState(new int[0],
- ObjectAnimator.ofFloat(view, "elevation", 0).setDuration(0));
-
- view.setStateListAnimator(sla);
- }
-
-}
diff --git a/android/support/design/widget/VisibilityAwareImageButton.java b/android/support/design/widget/VisibilityAwareImageButton.java
deleted file mode 100644
index d7a0b13..0000000
--- a/android/support/design/widget/VisibilityAwareImageButton.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.ImageButton;
-
-class VisibilityAwareImageButton extends ImageButton {
-
- private int mUserSetVisibility;
-
- public VisibilityAwareImageButton(Context context) {
- this(context, null);
- }
-
- public VisibilityAwareImageButton(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public VisibilityAwareImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- mUserSetVisibility = getVisibility();
- }
-
- @Override
- public void setVisibility(int visibility) {
- internalSetVisibility(visibility, true);
- }
-
- final void internalSetVisibility(int visibility, boolean fromUser) {
- super.setVisibility(visibility);
- if (fromUser) {
- mUserSetVisibility = visibility;
- }
- }
-
- final int getUserSetVisibility() {
- return mUserSetVisibility;
- }
-}
diff --git a/android/support/graphics/drawable/AndroidResources.java b/android/support/graphics/drawable/AndroidResources.java
index 31370a2..804c623 100644
--- a/android/support/graphics/drawable/AndroidResources.java
+++ b/android/support/graphics/drawable/AndroidResources.java
@@ -89,8 +89,7 @@
public static final int[] STYLEABLE_ANIMATOR = {
0x01010141, 0x01010198, 0x010101be, 0x010101bf,
- 0x010101c0, 0x010102de, 0x010102df, 0x010102e0,
- 0x0111009c
+ 0x010101c0, 0x010102de, 0x010102df, 0x010102e0
};
public static final int STYLEABLE_ANIMATOR_INTERPOLATOR = 0;
@@ -101,7 +100,6 @@
public static final int STYLEABLE_ANIMATOR_VALUE_FROM = 5;
public static final int STYLEABLE_ANIMATOR_VALUE_TO = 6;
public static final int STYLEABLE_ANIMATOR_VALUE_TYPE = 7;
- public static final int STYLEABLE_ANIMATOR_REMOVE_BEFORE_M_RELEASE = 8;
public static final int[] STYLEABLE_ANIMATOR_SET = {
0x010102e2
};
diff --git a/android/support/graphics/drawable/AnimatedVectorDrawableCompat.java b/android/support/graphics/drawable/AnimatedVectorDrawableCompat.java
index cff61bc..bc521cc 100644
--- a/android/support/graphics/drawable/AnimatedVectorDrawableCompat.java
+++ b/android/support/graphics/drawable/AnimatedVectorDrawableCompat.java
@@ -118,6 +118,9 @@
* <td>trimPathStart</td>
* </tr>
* <tr>
+ * <td>trimPathEnd</td>
+ * </tr>
+ * <tr>
* <td>trimPathOffset</td>
* </tr>
* </table>
diff --git a/android/support/graphics/drawable/AnimatorInflaterCompat.java b/android/support/graphics/drawable/AnimatorInflaterCompat.java
index cfededb..da522f6 100644
--- a/android/support/graphics/drawable/AnimatorInflaterCompat.java
+++ b/android/support/graphics/drawable/AnimatorInflaterCompat.java
@@ -463,7 +463,6 @@
// the previously sampled contours' total length.
for (int i = 0; i < numPoints; ++i) {
pathMeasure.getPosTan(currentDistance, position, null);
- pathMeasure.getPosTan(currentDistance, position, null);
mX[i] = position[0];
mY[i] = position[1];
diff --git a/android/support/media/ExifInterface.java b/android/support/media/ExifInterface.java
index eea69ab..6b437a6 100644
--- a/android/support/media/ExifInterface.java
+++ b/android/support/media/ExifInterface.java
@@ -23,6 +23,7 @@
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
import android.util.Log;
import android.util.Pair;
@@ -3550,6 +3551,7 @@
// Indices of Exif Ifd tag groups
/** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({IFD_TYPE_PRIMARY, IFD_TYPE_EXIF, IFD_TYPE_GPS, IFD_TYPE_INTEROPERABILITY,
IFD_TYPE_THUMBNAIL, IFD_TYPE_PREVIEW, IFD_TYPE_ORF_MAKER_NOTE,
@@ -4567,6 +4569,7 @@
* @param timeStamp number of milliseconds since Jan. 1, 1970, midnight local time.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public void setDateTime(long timeStamp) {
long sub = timeStamp % 1000;
setAttribute(TAG_DATETIME, sFormatter.format(new Date(timeStamp)));
@@ -4578,6 +4581,7 @@
* Returns -1 if the date time information if not available.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public long getDateTime() {
String dateTimeString = getAttribute(TAG_DATETIME);
if (dateTimeString == null
@@ -4614,6 +4618,7 @@
* Returns -1 if the date time information if not available.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public long getGpsDateTime() {
String date = getAttribute(TAG_GPS_DATESTAMP);
String time = getAttribute(TAG_GPS_TIMESTAMP);
diff --git a/android/support/media/tv/BasePreviewProgram.java b/android/support/media/tv/BasePreviewProgram.java
index eeaa5ea..816b1a1 100644
--- a/android/support/media/tv/BasePreviewProgram.java
+++ b/android/support/media/tv/BasePreviewProgram.java
@@ -15,6 +15,7 @@
*/
package android.support.media.tv;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.content.ContentValues;
@@ -39,6 +40,7 @@
*
* @hide
*/
+@RestrictTo(LIBRARY)
public abstract class BasePreviewProgram extends BaseProgram {
/**
* @hide
diff --git a/android/support/media/tv/BaseProgram.java b/android/support/media/tv/BaseProgram.java
index 23b5cf9..4c7882d 100644
--- a/android/support/media/tv/BaseProgram.java
+++ b/android/support/media/tv/BaseProgram.java
@@ -15,6 +15,7 @@
*/
package android.support.media.tv;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.content.ContentValues;
@@ -37,6 +38,7 @@
* {@link TvContractCompat}.
* @hide
*/
+@RestrictTo(LIBRARY)
public abstract class BaseProgram {
/**
* @hide
diff --git a/android/support/media/tv/TvContractCompat.java b/android/support/media/tv/TvContractCompat.java
index de4fd04..bd03bf1 100644
--- a/android/support/media/tv/TvContractCompat.java
+++ b/android/support/media/tv/TvContractCompat.java
@@ -2422,6 +2422,7 @@
/** Canonical genres for TV programs. */
public static final class Genres {
/** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@StringDef({
FAMILY_KIDS,
SPORTS,
diff --git a/android/support/multidex/MultiDex.java b/android/support/multidex/MultiDex.java
index ab7f668..2b681db 100644
--- a/android/support/multidex/MultiDex.java
+++ b/android/support/multidex/MultiDex.java
@@ -28,6 +28,7 @@
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -114,7 +115,8 @@
new File(applicationInfo.sourceDir),
new File(applicationInfo.dataDir),
CODE_CACHE_SECONDARY_FOLDER_NAME,
- NO_KEY_PREFIX);
+ NO_KEY_PREFIX,
+ true);
} catch (Exception e) {
Log.e(TAG, "MultiDex installation failure", e);
@@ -171,13 +173,15 @@
new File(instrumentationInfo.sourceDir),
dataDir,
instrumentationPrefix + CODE_CACHE_SECONDARY_FOLDER_NAME,
- instrumentationPrefix);
+ instrumentationPrefix,
+ false);
doInstallation(targetContext,
new File(applicationInfo.sourceDir),
dataDir,
CODE_CACHE_SECONDARY_FOLDER_NAME,
- NO_KEY_PREFIX);
+ NO_KEY_PREFIX,
+ false);
} catch (Exception e) {
Log.e(TAG, "MultiDex installation failure", e);
throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
@@ -192,11 +196,15 @@
* @param dataDir data directory to use for code cache simulation.
* @param secondaryFolderName name of the folder for storing extractions.
* @param prefsKeyPrefix prefix of all stored preference keys.
+ * @param reinstallOnPatchRecoverableException if set to true, will attempt a clean extraction
+ * if a possibly recoverable exception occurs during classloader patching.
*/
private static void doInstallation(Context mainContext, File sourceApk, File dataDir,
- String secondaryFolderName, String prefsKeyPrefix) throws IOException,
+ String secondaryFolderName, String prefsKeyPrefix,
+ boolean reinstallOnPatchRecoverableException) throws IOException,
IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
- InvocationTargetException, NoSuchMethodException {
+ InvocationTargetException, NoSuchMethodException, SecurityException,
+ ClassNotFoundException, InstantiationException {
synchronized (installedApk) {
if (installedApk.contains(sourceApk)) {
return;
@@ -245,9 +253,38 @@
}
File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
- List<? extends File> files =
- MultiDexExtractor.load(mainContext, sourceApk, dexDir, prefsKeyPrefix, false);
- installSecondaryDexes(loader, dexDir, files);
+ // MultiDexExtractor is taking the file lock and keeping it until it is closed.
+ // Keep it open during installSecondaryDexes and through forced extraction to ensure no
+ // extraction or optimizing dexopt is running in parallel.
+ MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
+ IOException closeException = null;
+ try {
+ List<? extends File> files =
+ extractor.load(mainContext, prefsKeyPrefix, false);
+ try {
+ installSecondaryDexes(loader, dexDir, files);
+ // Some IOException causes may be fixed by a clean extraction.
+ } catch (IOException e) {
+ if (!reinstallOnPatchRecoverableException) {
+ throw e;
+ }
+ Log.w(TAG, "Failed to install extracted secondary dex files, retrying with "
+ + "forced extraction", e);
+ files = extractor.load(mainContext, prefsKeyPrefix, true);
+ installSecondaryDexes(loader, dexDir, files);
+ }
+ } finally {
+ try {
+ extractor.close();
+ } catch (IOException e) {
+ // Delay throw of close exception to ensure we don't override some exception
+ // thrown during the try block.
+ closeException = e;
+ }
+ }
+ if (closeException != null) {
+ throw closeException;
+ }
}
}
@@ -305,12 +342,13 @@
private static void installSecondaryDexes(ClassLoader loader, File dexDir,
List<? extends File> files)
throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
- InvocationTargetException, NoSuchMethodException, IOException {
+ InvocationTargetException, NoSuchMethodException, IOException, SecurityException,
+ ClassNotFoundException, InstantiationException {
if (!files.isEmpty()) {
if (Build.VERSION.SDK_INT >= 19) {
V19.install(loader, files, dexDir);
} else if (Build.VERSION.SDK_INT >= 14) {
- V14.install(loader, files, dexDir);
+ V14.install(loader, files);
} else {
V4.install(loader, files);
}
@@ -460,11 +498,12 @@
*/
private static final class V19 {
- private static void install(ClassLoader loader,
+ static void install(ClassLoader loader,
List<? extends File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
- NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
+ NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
+ IOException {
/* The patched class loader is expected to be a descendant of
* dalvik.system.BaseDexClassLoader. We modify its
* dalvik.system.DexPathList pathList field to append additional DEX
@@ -500,6 +539,10 @@
}
suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
+
+ IOException exception = new IOException("I/O exception during makeDexElement");
+ exception.initCause(suppressedExceptions.get(0));
+ throw exception;
}
}
@@ -526,11 +569,16 @@
*/
private static final class V14 {
- private static void install(ClassLoader loader,
- List<? extends File> additionalClassPathEntries,
- File optimizedDirectory)
- throws IllegalArgumentException, IllegalAccessException,
- NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
+ private static final int EXTRACTED_SUFFIX_LENGTH =
+ MultiDexExtractor.EXTRACTED_SUFFIX.length();
+
+ private final Constructor<?> elementConstructor;
+
+ static void install(ClassLoader loader,
+ List<? extends File> additionalClassPathEntries)
+ throws IOException, SecurityException, IllegalArgumentException,
+ ClassNotFoundException, NoSuchMethodException, InstantiationException,
+ IllegalAccessException, InvocationTargetException, NoSuchFieldException {
/* The patched class loader is expected to be a descendant of
* dalvik.system.BaseDexClassLoader. We modify its
* dalvik.system.DexPathList pathList field to append additional DEX
@@ -538,22 +586,52 @@
*/
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
- expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
- new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
+ expandFieldArray(dexPathList, "dexElements",
+ new V14().makeDexElements(additionalClassPathEntries));
+ }
+
+ private V14() throws ClassNotFoundException, SecurityException, NoSuchMethodException {
+ Class<?> elementClass = Class.forName("dalvik.system.DexPathList$Element");
+ elementConstructor =
+ elementClass.getConstructor(File.class, ZipFile.class, DexFile.class);
+ elementConstructor.setAccessible(true);
}
/**
- * A wrapper around
- * {@code private static final dalvik.system.DexPathList#makeDexElements}.
+ * An emulation of {@code private static final dalvik.system.DexPathList#makeDexElements}
+ * accepting only extracted secondary dex files.
+ * OS version is catching IOException and just logging some of them, this version is letting
+ * them through.
*/
- private static Object[] makeDexElements(
- Object dexPathList, ArrayList<File> files, File optimizedDirectory)
- throws IllegalAccessException, InvocationTargetException,
- NoSuchMethodException {
- Method makeDexElements =
- findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
+ private Object[] makeDexElements(List<? extends File> files)
+ throws IOException, SecurityException, IllegalArgumentException,
+ InstantiationException, IllegalAccessException, InvocationTargetException {
+ Object[] elements = new Object[files.size()];
+ for (int i = 0; i < elements.length; i++) {
+ File file = files.get(i);
+ elements[i] = elementConstructor.newInstance(
+ file,
+ new ZipFile(file),
+ DexFile.loadDex(file.getPath(), optimizedPathFor(file), 0));
+ }
+ return elements;
+ }
- return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
+ /**
+ * Converts a zip file path of an extracted secondary dex to an output file path for an
+ * associated optimized dex file.
+ */
+ private static String optimizedPathFor(File path) {
+ // Any reproducible name ending with ".dex" should do but lets keep the same name
+ // as DexPathList.optimizedPathFor
+
+ File optimizedDirectory = path.getParentFile();
+ String fileName = path.getName();
+ String optimizedFileName =
+ fileName.substring(0, fileName.length() - EXTRACTED_SUFFIX_LENGTH)
+ + MultiDexExtractor.DEX_SUFFIX;
+ File result = new File(optimizedDirectory, optimizedFileName);
+ return result.getPath();
}
}
@@ -561,7 +639,7 @@
* Installer for platform versions 4 to 13.
*/
private static final class V4 {
- private static void install(ClassLoader loader,
+ static void install(ClassLoader loader,
List<? extends File> additionalClassPathEntries)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, IOException {
diff --git a/android/support/multidex/MultiDexExtractor.java b/android/support/multidex/MultiDexExtractor.java
index 39b6bf7..f0fd6d4 100644
--- a/android/support/multidex/MultiDexExtractor.java
+++ b/android/support/multidex/MultiDexExtractor.java
@@ -40,8 +40,10 @@
/**
* Exposes application secondary dex files as files in the application data
* directory.
+ * {@link MultiDexExtractor} is taking the file lock in the dex dir on creation and release it
+ * during close.
*/
-final class MultiDexExtractor {
+final class MultiDexExtractor implements Closeable {
/**
* Zip file containing one secondary dex file.
@@ -61,10 +63,10 @@
* {@code classes3.dex}, etc.
*/
private static final String DEX_PREFIX = "classes";
- private static final String DEX_SUFFIX = ".dex";
+ static final String DEX_SUFFIX = ".dex";
private static final String EXTRACTED_NAME_EXT = ".classes";
- private static final String EXTRACTED_SUFFIX = ".zip";
+ static final String EXTRACTED_SUFFIX = ".zip";
private static final int MAX_EXTRACT_ATTEMPTS = 3;
private static final String PREFS_FILE = "multidex.version";
@@ -82,6 +84,35 @@
private static final long NO_VALUE = -1L;
private static final String LOCK_FILENAME = "MultiDex.lock";
+ private final File sourceApk;
+ private final long sourceCrc;
+ private final File dexDir;
+ private final RandomAccessFile lockRaf;
+ private final FileChannel lockChannel;
+ private final FileLock cacheLock;
+
+ MultiDexExtractor(File sourceApk, File dexDir) throws IOException {
+ Log.i(TAG, "MultiDexExtractor(" + sourceApk.getPath() + ", " + dexDir.getPath() + ")");
+ this.sourceApk = sourceApk;
+ this.dexDir = dexDir;
+ sourceCrc = getZipCrc(sourceApk);
+ File lockFile = new File(dexDir, LOCK_FILENAME);
+ lockRaf = new RandomAccessFile(lockFile, "rw");
+ try {
+ lockChannel = lockRaf.getChannel();
+ try {
+ Log.i(TAG, "Blocking on lock " + lockFile.getPath());
+ cacheLock = lockChannel.lock();
+ } catch (IOException | RuntimeException | Error e) {
+ closeQuietly(lockChannel);
+ throw e;
+ }
+ Log.i(TAG, lockFile.getPath() + " locked");
+ } catch (IOException | RuntimeException | Error e) {
+ closeQuietly(lockRaf);
+ throw e;
+ }
+ }
/**
* Extracts application secondary dexes into files in the application data
@@ -92,74 +123,54 @@
* @throws IOException if encounters a problem while reading or writing
* secondary dex files
*/
- static List<? extends File> load(Context context, File sourceApk, File dexDir,
- String prefsKeyPrefix,
- boolean forceReload) throws IOException {
+ List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload)
+ throws IOException {
Log.i(TAG, "MultiDexExtractor.load(" + sourceApk.getPath() + ", " + forceReload + ", " +
prefsKeyPrefix + ")");
- long currentCrc = getZipCrc(sourceApk);
-
- // Validity check and extraction must be done only while the lock file has been taken.
- File lockFile = new File(dexDir, LOCK_FILENAME);
- RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw");
- FileChannel lockChannel = null;
- FileLock cacheLock = null;
- List<ExtractedDex> files;
- IOException releaseLockException = null;
- try {
- lockChannel = lockRaf.getChannel();
- Log.i(TAG, "Blocking on lock " + lockFile.getPath());
- cacheLock = lockChannel.lock();
- Log.i(TAG, lockFile.getPath() + " locked");
-
- if (!forceReload && !isModified(context, sourceApk, currentCrc, prefsKeyPrefix)) {
- try {
- files = loadExistingExtractions(context, sourceApk, dexDir, prefsKeyPrefix);
- } catch (IOException ioe) {
- Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
- + " falling back to fresh extraction", ioe);
- files = performExtractions(sourceApk, dexDir);
- putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), currentCrc,
- files);
- }
- } else {
- Log.i(TAG, "Detected that extraction must be performed.");
- files = performExtractions(sourceApk, dexDir);
- putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), currentCrc,
- files);
- }
- } finally {
- if (cacheLock != null) {
- try {
- cacheLock.release();
- } catch (IOException e) {
- Log.e(TAG, "Failed to release lock on " + lockFile.getPath());
- // Exception while releasing the lock is bad, we want to report it, but not at
- // the price of overriding any already pending exception.
- releaseLockException = e;
- }
- }
- if (lockChannel != null) {
- closeQuietly(lockChannel);
- }
- closeQuietly(lockRaf);
+ if (!cacheLock.isValid()) {
+ throw new IllegalStateException("MultiDexExtractor was closed");
}
- if (releaseLockException != null) {
- throw releaseLockException;
+ List<ExtractedDex> files;
+ if (!forceReload && !isModified(context, sourceApk, sourceCrc, prefsKeyPrefix)) {
+ try {
+ files = loadExistingExtractions(context, prefsKeyPrefix);
+ } catch (IOException ioe) {
+ Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
+ + " falling back to fresh extraction", ioe);
+ files = performExtractions();
+ putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
+ files);
+ }
+ } else {
+ if (forceReload) {
+ Log.i(TAG, "Forced extraction must be performed.");
+ } else {
+ Log.i(TAG, "Detected that extraction must be performed.");
+ }
+ files = performExtractions();
+ putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
+ files);
}
Log.i(TAG, "load found " + files.size() + " secondary dex files");
return files;
}
+ @Override
+ public void close() throws IOException {
+ cacheLock.release();
+ lockChannel.close();
+ lockRaf.close();
+ }
+
/**
* Load previously extracted secondary dex files. Should be called only while owning the lock on
* {@link #LOCK_FILENAME}.
*/
- private static List<ExtractedDex> loadExistingExtractions(
- Context context, File sourceApk, File dexDir,
+ private List<ExtractedDex> loadExistingExtractions(
+ Context context,
String prefsKeyPrefix)
throws IOException {
Log.i(TAG, "loading existing secondary dex files");
@@ -228,16 +239,14 @@
return computedValue;
}
- private static List<ExtractedDex> performExtractions(File sourceApk, File dexDir)
- throws IOException {
+ private List<ExtractedDex> performExtractions() throws IOException {
final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
- // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
- // contains a secondary dex file in there is not consistent with the latest apk. Otherwise,
- // multi-process race conditions can cause a crash loop where one process deletes the zip
- // while another had created it.
- prepareDexDir(dexDir, extractedFilePrefix);
+ // It is safe to fully clear the dex dir because we own the file lock so no other process is
+ // extracting or running optimizing dexopt. It may cause crash of already running
+ // applications if for whatever reason we end up extracting again over a valid extraction.
+ clearDexDir();
List<ExtractedDex> files = new ArrayList<ExtractedDex>();
@@ -272,9 +281,9 @@
}
// Log size and crc of the extracted zip file
- Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed") +
- " - length " + extractedFile.getAbsolutePath() + ": " +
- extractedFile.length() + " - crc: " + extractedFile.crc);
+ Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed")
+ + " '" + extractedFile.getAbsolutePath() + "': length "
+ + extractedFile.length() + " - crc: " + extractedFile.crc);
if (!isExtractionSuccessful) {
// Delete the extracted file
extractedFile.delete();
@@ -339,19 +348,15 @@
}
/**
- * This removes old files.
+ * Clear the dex dir from all files but the lock.
*/
- private static void prepareDexDir(File dexDir, final String extractedFilePrefix) {
- FileFilter filter = new FileFilter() {
-
+ private void clearDexDir() {
+ File[] files = dexDir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
- String name = pathname.getName();
- return !(name.startsWith(extractedFilePrefix)
- || name.equals(LOCK_FILENAME));
+ return !pathname.getName().equals(LOCK_FILENAME);
}
- };
- File[] files = dexDir.listFiles(filter);
+ });
if (files == null) {
Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
return;
diff --git a/android/support/transition/TransitionSet.java b/android/support/transition/TransitionSet.java
index 404245a..24075bb 100644
--- a/android/support/transition/TransitionSet.java
+++ b/android/support/transition/TransitionSet.java
@@ -51,7 +51,7 @@
* transition on the affected view targets:</p>
* <pre>
* <transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
- * android:ordering="sequential">
+ * android:transitionOrdering="sequential">
* <fade/>
* <changeBounds/>
* </transitionSet>
@@ -561,6 +561,15 @@
}
@Override
+ public void setPropagation(TransitionPropagation propagation) {
+ super.setPropagation(propagation);
+ int numTransitions = mTransitions.size();
+ for (int i = 0; i < numTransitions; ++i) {
+ mTransitions.get(i).setPropagation(propagation);
+ }
+ }
+
+ @Override
public void setEpicenterCallback(EpicenterCallback epicenterCallback) {
super.setEpicenterCallback(epicenterCallback);
int numTransitions = mTransitions.size();
diff --git a/android/support/transition/TransitionSetTest.java b/android/support/transition/TransitionSetTest.java
index aec9ecb..d82cd49 100644
--- a/android/support/transition/TransitionSetTest.java
+++ b/android/support/transition/TransitionSetTest.java
@@ -120,4 +120,12 @@
assertThat(mTransition.getTargetTypes(), hasSize(0));
}
+ @Test
+ public void testSetPropagation() {
+ final TransitionPropagation propagation = new SidePropagation();
+ mTransitionSet.setPropagation(propagation);
+ assertThat(mTransitionSet.getPropagation(), is(propagation));
+ assertThat(mTransition.getPropagation(), is(propagation));
+ }
+
}
diff --git a/android/support/v13/view/DragAndDropPermissionsCompat.java b/android/support/v13/view/DragAndDropPermissionsCompat.java
index 13ed203..5fe61da 100644
--- a/android/support/v13/view/DragAndDropPermissionsCompat.java
+++ b/android/support/v13/view/DragAndDropPermissionsCompat.java
@@ -20,57 +20,16 @@
import android.app.Activity;
import android.os.Build;
-import android.support.annotation.RequiresApi;
+import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.view.DragAndDropPermissions;
import android.view.DragEvent;
/**
- * Helper for accessing features in {@link android.view.DragAndDropPermissions}
- * introduced after API level 13 in a backwards compatible fashion.
+ * Helper for accessing features in {@link android.view.DragAndDropPermissions} a backwards
+ * compatible fashion.
*/
public final class DragAndDropPermissionsCompat {
-
- interface DragAndDropPermissionsCompatImpl {
- Object request(Activity activity, DragEvent dragEvent);
- void release(Object dragAndDropPermissions);
- }
-
- static class BaseDragAndDropPermissionsCompatImpl implements DragAndDropPermissionsCompatImpl {
- @Override
- public Object request(Activity activity, DragEvent dragEvent) {
- return null;
- }
-
- @Override
- public void release(Object dragAndDropPermissions) {
- // no-op
- }
- }
-
- @RequiresApi(24)
- static class Api24DragAndDropPermissionsCompatImpl
- extends BaseDragAndDropPermissionsCompatImpl {
- @Override
- public Object request(Activity activity, DragEvent dragEvent) {
- return activity.requestDragAndDropPermissions(dragEvent);
- }
-
- @Override
- public void release(Object dragAndDropPermissions) {
- ((DragAndDropPermissions) dragAndDropPermissions).release();
- }
- }
-
- private static DragAndDropPermissionsCompatImpl IMPL;
- static {
- if (Build.VERSION.SDK_INT >= 24) {
- IMPL = new Api24DragAndDropPermissionsCompatImpl();
- } else {
- IMPL = new BaseDragAndDropPermissionsCompatImpl();
- }
- }
-
private Object mDragAndDropPermissions;
private DragAndDropPermissionsCompat(Object dragAndDropPermissions) {
@@ -79,18 +38,24 @@
/** @hide */
@RestrictTo(LIBRARY_GROUP)
+ @Nullable
public static DragAndDropPermissionsCompat request(Activity activity, DragEvent dragEvent) {
- Object dragAndDropPermissions = IMPL.request(activity, dragEvent);
- if (dragAndDropPermissions != null) {
- return new DragAndDropPermissionsCompat(dragAndDropPermissions);
+ if (Build.VERSION.SDK_INT >= 24) {
+ DragAndDropPermissions dragAndDropPermissions =
+ activity.requestDragAndDropPermissions(dragEvent);
+ if (dragAndDropPermissions != null) {
+ return new DragAndDropPermissionsCompat(dragAndDropPermissions);
+ }
}
return null;
}
- /*
+ /**
* Revoke the permission grant explicitly.
*/
public void release() {
- IMPL.release(mDragAndDropPermissions);
+ if (Build.VERSION.SDK_INT >= 24) {
+ ((DragAndDropPermissions) mDragAndDropPermissions).release();
+ }
}
}
diff --git a/android/support/v13/view/DragStartHelper.java b/android/support/v13/view/DragStartHelper.java
index 85bc2f3..f8aed92 100644
--- a/android/support/v13/view/DragStartHelper.java
+++ b/android/support/v13/view/DragStartHelper.java
@@ -69,8 +69,8 @@
* </pre>
*/
public class DragStartHelper {
- final private View mView;
- final private OnDragStartListener mListener;
+ private final View mView;
+ private final OnDragStartListener mListener;
private int mLastTouchX, mLastTouchY;
private boolean mDragging;
diff --git a/android/support/v13/view/inputmethod/EditorInfoCompat.java b/android/support/v13/view/inputmethod/EditorInfoCompat.java
index 92743c2..309877d 100644
--- a/android/support/v13/view/inputmethod/EditorInfoCompat.java
+++ b/android/support/v13/view/inputmethod/EditorInfoCompat.java
@@ -16,7 +16,6 @@
package android.support.v13.view.inputmethod;
-import android.support.annotation.RequiresApi;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
@@ -24,8 +23,7 @@
import android.view.inputmethod.EditorInfo;
/**
- * Helper for accessing features in {@link EditorInfo} introduced after API level 13 in a backwards
- * compatible fashion.
+ * Helper for accessing features in {@link EditorInfo} in a backwards compatible fashion.
*/
public final class EditorInfoCompat {
@@ -69,63 +67,10 @@
*/
public static final int IME_FLAG_FORCE_ASCII = 0x80000000;
- private interface EditorInfoCompatImpl {
- void setContentMimeTypes(@NonNull EditorInfo editorInfo,
- @Nullable String[] contentMimeTypes);
- @NonNull
- String[] getContentMimeTypes(@NonNull EditorInfo editorInfo);
- }
-
private static final String[] EMPTY_STRING_ARRAY = new String[0];
- private static final class EditorInfoCompatBaseImpl implements EditorInfoCompatImpl {
- private static String CONTENT_MIME_TYPES_KEY =
- "android.support.v13.view.inputmethod.EditorInfoCompat.CONTENT_MIME_TYPES";
-
- @Override
- public void setContentMimeTypes(@NonNull EditorInfo editorInfo,
- @Nullable String[] contentMimeTypes) {
- if (editorInfo.extras == null) {
- editorInfo.extras = new Bundle();
- }
- editorInfo.extras.putStringArray(CONTENT_MIME_TYPES_KEY, contentMimeTypes);
- }
-
- @NonNull
- @Override
- public String[] getContentMimeTypes(@NonNull EditorInfo editorInfo) {
- if (editorInfo.extras == null) {
- return EMPTY_STRING_ARRAY;
- }
- String[] result = editorInfo.extras.getStringArray(CONTENT_MIME_TYPES_KEY);
- return result != null ? result : EMPTY_STRING_ARRAY;
- }
- }
-
- @RequiresApi(25)
- private static final class EditorInfoCompatApi25Impl implements EditorInfoCompatImpl {
- @Override
- public void setContentMimeTypes(@NonNull EditorInfo editorInfo,
- @Nullable String[] contentMimeTypes) {
- editorInfo.contentMimeTypes = contentMimeTypes;
- }
-
- @NonNull
- @Override
- public String[] getContentMimeTypes(@NonNull EditorInfo editorInfo) {
- final String[] result = editorInfo.contentMimeTypes;
- return result != null ? result : EMPTY_STRING_ARRAY;
- }
- }
-
- private static final EditorInfoCompatImpl IMPL;
- static {
- if (Build.VERSION.SDK_INT >= 25) {
- IMPL = new EditorInfoCompatApi25Impl();
- } else {
- IMPL = new EditorInfoCompatBaseImpl();
- }
- }
+ private static final String CONTENT_MIME_TYPES_KEY =
+ "android.support.v13.view.inputmethod.EditorInfoCompat.CONTENT_MIME_TYPES";
/**
* Sets MIME types that can be accepted by the target editor if the IME calls
@@ -140,7 +85,14 @@
*/
public static void setContentMimeTypes(@NonNull EditorInfo editorInfo,
@Nullable String[] contentMimeTypes) {
- IMPL.setContentMimeTypes(editorInfo, contentMimeTypes);
+ if (Build.VERSION.SDK_INT >= 25) {
+ editorInfo.contentMimeTypes = contentMimeTypes;
+ } else {
+ if (editorInfo.extras == null) {
+ editorInfo.extras = new Bundle();
+ }
+ editorInfo.extras.putStringArray(CONTENT_MIME_TYPES_KEY, contentMimeTypes);
+ }
}
/**
@@ -155,7 +107,16 @@
*/
@NonNull
public static String[] getContentMimeTypes(EditorInfo editorInfo) {
- return IMPL.getContentMimeTypes(editorInfo);
+ if (Build.VERSION.SDK_INT >= 25) {
+ final String[] result = editorInfo.contentMimeTypes;
+ return result != null ? result : EMPTY_STRING_ARRAY;
+ } else {
+ if (editorInfo.extras == null) {
+ return EMPTY_STRING_ARRAY;
+ }
+ String[] result = editorInfo.extras.getStringArray(CONTENT_MIME_TYPES_KEY);
+ return result != null ? result : EMPTY_STRING_ARRAY;
+ }
}
}
diff --git a/android/support/v13/view/inputmethod/InputConnectionCompat.java b/android/support/v13/view/inputmethod/InputConnectionCompat.java
index 5999575..d77389b 100644
--- a/android/support/v13/view/inputmethod/InputConnectionCompat.java
+++ b/android/support/v13/view/inputmethod/InputConnectionCompat.java
@@ -16,7 +16,6 @@
package android.support.v13.view.inputmethod;
-import android.support.annotation.RequiresApi;
import android.content.ClipDescription;
import android.net.Uri;
import android.os.Build;
@@ -36,138 +35,50 @@
*/
public final class InputConnectionCompat {
- private interface InputConnectionCompatImpl {
- boolean commitContent(@NonNull InputConnection inputConnection,
- @NonNull InputContentInfoCompat inputContentInfo, int flags, @Nullable Bundle opts);
+ private static final String COMMIT_CONTENT_ACTION =
+ "android.support.v13.view.inputmethod.InputConnectionCompat.COMMIT_CONTENT";
+ private static final String COMMIT_CONTENT_CONTENT_URI_KEY =
+ "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_URI";
+ private static final String COMMIT_CONTENT_DESCRIPTION_KEY =
+ "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_DESCRIPTION";
+ private static final String COMMIT_CONTENT_LINK_URI_KEY =
+ "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_LINK_URI";
+ private static final String COMMIT_CONTENT_OPTS_KEY =
+ "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_OPTS";
+ private static final String COMMIT_CONTENT_FLAGS_KEY =
+ "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_FLAGS";
+ private static final String COMMIT_CONTENT_RESULT_RECEIVER =
+ "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_RESULT_RECEIVER";
- @NonNull
- InputConnection createWrapper(@NonNull InputConnection ic,
- @NonNull EditorInfo editorInfo, @NonNull OnCommitContentListener callback);
- }
-
- static final class InputContentInfoCompatBaseImpl implements InputConnectionCompatImpl {
-
- private static String COMMIT_CONTENT_ACTION =
- "android.support.v13.view.inputmethod.InputConnectionCompat.COMMIT_CONTENT";
- private static String COMMIT_CONTENT_CONTENT_URI_KEY =
- "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_URI";
- private static String COMMIT_CONTENT_DESCRIPTION_KEY =
- "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_DESCRIPTION";
- private static String COMMIT_CONTENT_LINK_URI_KEY =
- "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_LINK_URI";
- private static String COMMIT_CONTENT_OPTS_KEY =
- "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_OPTS";
- private static String COMMIT_CONTENT_FLAGS_KEY =
- "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_FLAGS";
- private static String COMMIT_CONTENT_RESULT_RECEIVER =
- "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_RESULT_RECEIVER";
-
- @Override
- public boolean commitContent(@NonNull InputConnection inputConnection,
- @NonNull InputContentInfoCompat inputContentInfo, int flags,
- @Nullable Bundle opts) {
- final Bundle params = new Bundle();
- params.putParcelable(COMMIT_CONTENT_CONTENT_URI_KEY, inputContentInfo.getContentUri());
- params.putParcelable(COMMIT_CONTENT_DESCRIPTION_KEY, inputContentInfo.getDescription());
- params.putParcelable(COMMIT_CONTENT_LINK_URI_KEY, inputContentInfo.getLinkUri());
- params.putInt(COMMIT_CONTENT_FLAGS_KEY, flags);
- params.putParcelable(COMMIT_CONTENT_OPTS_KEY, opts);
- // TODO: Support COMMIT_CONTENT_RESULT_RECEIVER.
- return inputConnection.performPrivateCommand(COMMIT_CONTENT_ACTION, params);
+ static boolean handlePerformPrivateCommand(
+ @Nullable String action,
+ @NonNull Bundle data,
+ @NonNull OnCommitContentListener onCommitContentListener) {
+ if (!TextUtils.equals(COMMIT_CONTENT_ACTION, action)) {
+ return false;
}
-
- @NonNull
- @Override
- public InputConnection createWrapper(@NonNull InputConnection ic,
- @NonNull EditorInfo editorInfo,
- @NonNull OnCommitContentListener onCommitContentListener) {
- String[] contentMimeTypes = EditorInfoCompat.getContentMimeTypes(editorInfo);
- if (contentMimeTypes.length == 0) {
- return ic;
+ if (data == null) {
+ return false;
+ }
+ ResultReceiver resultReceiver = null;
+ boolean result = false;
+ try {
+ resultReceiver = data.getParcelable(COMMIT_CONTENT_RESULT_RECEIVER);
+ final Uri contentUri = data.getParcelable(COMMIT_CONTENT_CONTENT_URI_KEY);
+ final ClipDescription description = data.getParcelable(
+ COMMIT_CONTENT_DESCRIPTION_KEY);
+ final Uri linkUri = data.getParcelable(COMMIT_CONTENT_LINK_URI_KEY);
+ final int flags = data.getInt(COMMIT_CONTENT_FLAGS_KEY);
+ final Bundle opts = data.getParcelable(COMMIT_CONTENT_OPTS_KEY);
+ final InputContentInfoCompat inputContentInfo =
+ new InputContentInfoCompat(contentUri, description, linkUri);
+ result = onCommitContentListener.onCommitContent(inputContentInfo, flags, opts);
+ } finally {
+ if (resultReceiver != null) {
+ resultReceiver.send(result ? 1 : 0, null);
}
- final OnCommitContentListener listener = onCommitContentListener;
- return new InputConnectionWrapper(ic, false /* mutable */) {
- @Override
- public boolean performPrivateCommand(String action, Bundle data) {
- if (InputContentInfoCompatBaseImpl.handlePerformPrivateCommand(action, data,
- listener)) {
- return true;
- }
- return super.performPrivateCommand(action, data);
- }
- };
}
-
- static boolean handlePerformPrivateCommand(
- @Nullable String action,
- @NonNull Bundle data,
- @NonNull OnCommitContentListener onCommitContentListener) {
- if (!TextUtils.equals(COMMIT_CONTENT_ACTION, action)) {
- return false;
- }
- if (data == null) {
- return false;
- }
- ResultReceiver resultReceiver = null;
- boolean result = false;
- try {
- resultReceiver = data.getParcelable(COMMIT_CONTENT_RESULT_RECEIVER);
- final Uri contentUri = data.getParcelable(COMMIT_CONTENT_CONTENT_URI_KEY);
- final ClipDescription description = data.getParcelable(
- COMMIT_CONTENT_DESCRIPTION_KEY);
- final Uri linkUri = data.getParcelable(COMMIT_CONTENT_LINK_URI_KEY);
- final int flags = data.getInt(COMMIT_CONTENT_FLAGS_KEY);
- final Bundle opts = data.getParcelable(COMMIT_CONTENT_OPTS_KEY);
- final InputContentInfoCompat inputContentInfo =
- new InputContentInfoCompat(contentUri, description, linkUri);
- result = onCommitContentListener.onCommitContent(inputContentInfo, flags, opts);
- } finally {
- if (resultReceiver != null) {
- resultReceiver.send(result ? 1 : 0, null);
- }
- }
- return result;
- }
- }
-
- @RequiresApi(25)
- private static final class InputContentInfoCompatApi25Impl
- implements InputConnectionCompatImpl {
- @Override
- public boolean commitContent(@NonNull InputConnection inputConnection,
- @NonNull InputContentInfoCompat inputContentInfo, int flags,
- @Nullable Bundle opts) {
- return inputConnection.commitContent((InputContentInfo) inputContentInfo.unwrap(),
- flags, opts);
- }
-
- @Nullable
- @Override
- public InputConnection createWrapper(
- @Nullable InputConnection inputConnection, @NonNull EditorInfo editorInfo,
- @Nullable OnCommitContentListener onCommitContentListener) {
- final OnCommitContentListener listener = onCommitContentListener;
- return new InputConnectionWrapper(inputConnection, false /* mutable */) {
- @Override
- public boolean commitContent(InputContentInfo inputContentInfo, int flags,
- Bundle opts) {
- if (listener.onCommitContent(InputContentInfoCompat.wrap(inputContentInfo),
- flags, opts)) {
- return true;
- }
- return super.commitContent(inputContentInfo, flags, opts);
- }
- };
- }
- }
-
- private static final InputConnectionCompatImpl IMPL;
- static {
- if (Build.VERSION.SDK_INT >= 25) {
- IMPL = new InputContentInfoCompatApi25Impl();
- } else {
- IMPL = new InputContentInfoCompatBaseImpl();
- }
+ return result;
}
/**
@@ -196,7 +107,19 @@
return false;
}
- return IMPL.commitContent(inputConnection, inputContentInfo, flags, opts);
+ if (Build.VERSION.SDK_INT >= 25) {
+ return inputConnection.commitContent(
+ (InputContentInfo) inputContentInfo.unwrap(), flags, opts);
+ } else {
+ final Bundle params = new Bundle();
+ params.putParcelable(COMMIT_CONTENT_CONTENT_URI_KEY, inputContentInfo.getContentUri());
+ params.putParcelable(COMMIT_CONTENT_DESCRIPTION_KEY, inputContentInfo.getDescription());
+ params.putParcelable(COMMIT_CONTENT_LINK_URI_KEY, inputContentInfo.getLinkUri());
+ params.putInt(COMMIT_CONTENT_FLAGS_KEY, flags);
+ params.putParcelable(COMMIT_CONTENT_OPTS_KEY, opts);
+ // TODO: Support COMMIT_CONTENT_RESULT_RECEIVER.
+ return inputConnection.performPrivateCommand(COMMIT_CONTENT_ACTION, params);
+ }
}
/**
@@ -276,7 +199,35 @@
if (onCommitContentListener == null) {
throw new IllegalArgumentException("onCommitContentListener must be non-null");
}
- return IMPL.createWrapper(inputConnection, editorInfo, onCommitContentListener);
+ if (Build.VERSION.SDK_INT >= 25) {
+ final OnCommitContentListener listener = onCommitContentListener;
+ return new InputConnectionWrapper(inputConnection, false /* mutable */) {
+ @Override
+ public boolean commitContent(InputContentInfo inputContentInfo, int flags,
+ Bundle opts) {
+ if (listener.onCommitContent(InputContentInfoCompat.wrap(inputContentInfo),
+ flags, opts)) {
+ return true;
+ }
+ return super.commitContent(inputContentInfo, flags, opts);
+ }
+ };
+ } else {
+ String[] contentMimeTypes = EditorInfoCompat.getContentMimeTypes(editorInfo);
+ if (contentMimeTypes.length == 0) {
+ return inputConnection;
+ }
+ final OnCommitContentListener listener = onCommitContentListener;
+ return new InputConnectionWrapper(inputConnection, false /* mutable */) {
+ @Override
+ public boolean performPrivateCommand(String action, Bundle data) {
+ if (InputConnectionCompat.handlePerformPrivateCommand(action, data, listener)) {
+ return true;
+ }
+ return super.performPrivateCommand(action, data);
+ }
+ };
+ }
}
}
diff --git a/android/support/v14/preference/EditTextPreferenceDialogFragment.java b/android/support/v14/preference/EditTextPreferenceDialogFragment.java
index 3ee5872..23b8828 100644
--- a/android/support/v14/preference/EditTextPreferenceDialogFragment.java
+++ b/android/support/v14/preference/EditTextPreferenceDialogFragment.java
@@ -11,7 +11,7 @@
* 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
+ * limitations under the License.
*/
package android.support.v14.preference;
@@ -65,8 +65,8 @@
mEditText = (EditText) view.findViewById(android.R.id.edit);
if (mEditText == null) {
- throw new IllegalStateException("Dialog view must contain an EditText with id" +
- " @android:id/edit");
+ throw new IllegalStateException("Dialog view must contain an EditText with id"
+ + " @android:id/edit");
}
mEditText.setText(mText);
diff --git a/android/support/v14/preference/ListPreferenceDialogFragment.java b/android/support/v14/preference/ListPreferenceDialogFragment.java
index 6119071..5374cd5 100644
--- a/android/support/v14/preference/ListPreferenceDialogFragment.java
+++ b/android/support/v14/preference/ListPreferenceDialogFragment.java
@@ -11,7 +11,7 @@
* 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
+ * limitations under the License.
*/
package android.support.v14.preference;
diff --git a/android/support/v14/preference/MultiSelectListPreference.java b/android/support/v14/preference/MultiSelectListPreference.java
index f34b7dc..16351fe 100644
--- a/android/support/v14/preference/MultiSelectListPreference.java
+++ b/android/support/v14/preference/MultiSelectListPreference.java
@@ -11,7 +11,7 @@
* 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
+ * limitations under the License.
*/
package android.support.v14.preference;
@@ -23,6 +23,7 @@
import android.support.annotation.ArrayRes;
import android.support.annotation.NonNull;
import android.support.v4.content.res.TypedArrayUtils;
+import android.support.v7.preference.R;
import android.support.v7.preference.internal.AbstractMultiSelectListPreference;
import android.util.AttributeSet;
@@ -35,7 +36,7 @@
* a dialog.
* <p>
* This preference will store a set of strings into the SharedPreferences.
- * This set will contain one or more values from the
+ * This set will contain one or more mValues from the
* {@link #setEntryValues(CharSequence[])} array.
*
* @attr name android:entries
@@ -51,16 +52,16 @@
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray a = context.obtainStyledAttributes(attrs,
- android.support.v7.preference.R.styleable.MultiSelectListPreference, defStyleAttr,
+ R.styleable.MultiSelectListPreference, defStyleAttr,
defStyleRes);
mEntries = TypedArrayUtils.getTextArray(a,
- android.support.v7.preference.R.styleable.MultiSelectListPreference_entries,
- android.support.v7.preference.R.styleable.MultiSelectListPreference_android_entries);
+ R.styleable.MultiSelectListPreference_entries,
+ R.styleable.MultiSelectListPreference_android_entries);
mEntryValues = TypedArrayUtils.getTextArray(a,
- android.support.v7.preference.R.styleable.MultiSelectListPreference_entryValues,
- android.support.v7.preference.R.styleable.MultiSelectListPreference_android_entryValues);
+ R.styleable.MultiSelectListPreference_entryValues,
+ R.styleable.MultiSelectListPreference_android_entryValues);
a.recycle();
}
@@ -71,7 +72,7 @@
public MultiSelectListPreference(Context context, AttributeSet attrs) {
this(context, attrs, TypedArrayUtils.getAttr(context,
- android.support.v7.preference.R.attr.dialogPreferenceStyle,
+ R.attr.dialogPreferenceStyle,
android.R.attr.dialogPreferenceStyle));
}
@@ -116,7 +117,7 @@
* entries is selected. If a user clicks on the second item in entries, the
* second item in this array will be saved to the preference.
*
- * @param entryValues The array to be used as values to save for the preference.
+ * @param entryValues The array to be used as mValues to save for the preference.
*/
public void setEntryValues(CharSequence[] entryValues) {
mEntryValues = entryValues;
@@ -124,16 +125,16 @@
/**
* @see #setEntryValues(CharSequence[])
- * @param entryValuesResId The entry values array as a resource.
+ * @param entryValuesResId The entry mValues array as a resource.
*/
public void setEntryValues(@ArrayRes int entryValuesResId) {
setEntryValues(getContext().getResources().getTextArray(entryValuesResId));
}
/**
- * Returns the array of values to be saved for the preference.
+ * Returns the array of mValues to be saved for the preference.
*
- * @return The array of values.
+ * @return The array of mValues.
*/
@Override
public CharSequence[] getEntryValues() {
@@ -144,7 +145,7 @@
* Sets the value of the key. This should contain entries in
* {@link #getEntryValues()}.
*
- * @param values The values to set for the key.
+ * @param values The mValues to set for the key.
*/
@Override
public void setValues(Set<String> values) {
@@ -163,7 +164,7 @@
}
/**
- * Returns the index of the given value (in the entry values array).
+ * Returns the index of the given value (in the entry mValues array).
*
* @param value The value whose index should be returned.
* @return The index of the value, or -1 if not found.
@@ -219,7 +220,7 @@
}
final SavedState myState = new SavedState(superState);
- myState.values = getValues();
+ myState.mValues = getValues();
return myState;
}
@@ -233,31 +234,31 @@
SavedState myState = (SavedState) state;
super.onRestoreInstanceState(myState.getSuperState());
- setValues(myState.values);
+ setValues(myState.mValues);
}
private static class SavedState extends BaseSavedState {
- Set<String> values;
+ Set<String> mValues;
- public SavedState(Parcel source) {
+ SavedState(Parcel source) {
super(source);
final int size = source.readInt();
- values = new HashSet<>();
+ mValues = new HashSet<>();
String[] strings = new String[size];
source.readStringArray(strings);
- Collections.addAll(values, strings);
+ Collections.addAll(mValues, strings);
}
- public SavedState(Parcelable superState) {
+ SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
super.writeToParcel(dest, flags);
- dest.writeInt(values.size());
- dest.writeStringArray(values.toArray(new String[values.size()]));
+ dest.writeInt(mValues.size());
+ dest.writeStringArray(mValues.toArray(new String[mValues.size()]));
}
public static final Parcelable.Creator<SavedState> CREATOR =
diff --git a/android/support/v14/preference/MultiSelectListPreferenceDialogFragment.java b/android/support/v14/preference/MultiSelectListPreferenceDialogFragment.java
index 8192583..db81644 100644
--- a/android/support/v14/preference/MultiSelectListPreferenceDialogFragment.java
+++ b/android/support/v14/preference/MultiSelectListPreferenceDialogFragment.java
@@ -11,7 +11,7 @@
* 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
+ * limitations under the License.
*/
package android.support.v14.preference;
@@ -60,8 +60,8 @@
if (preference.getEntries() == null || preference.getEntryValues() == null) {
throw new IllegalStateException(
- "MultiSelectListPreference requires an entries array and " +
- "an entryValues array.");
+ "MultiSelectListPreference requires an entries array and "
+ + "an entryValues array.");
}
mNewValues.clear();
diff --git a/android/support/v14/preference/PreferenceDialogFragment.java b/android/support/v14/preference/PreferenceDialogFragment.java
index e7b9f40..a4ae4a9 100644
--- a/android/support/v14/preference/PreferenceDialogFragment.java
+++ b/android/support/v14/preference/PreferenceDialogFragment.java
@@ -11,7 +11,7 @@
* 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
+ * limitations under the License.
*/
package android.support.v14.preference;
@@ -78,8 +78,8 @@
final Fragment rawFragment = getTargetFragment();
if (!(rawFragment instanceof DialogPreference.TargetFragment)) {
- throw new IllegalStateException("Target fragment must implement TargetFragment" +
- " interface");
+ throw new IllegalStateException("Target fragment must implement TargetFragment"
+ + " interface");
}
final DialogPreference.TargetFragment fragment =
@@ -132,9 +132,9 @@
}
}
+ @NonNull
@Override
- public @NonNull
- Dialog onCreateDialog(Bundle savedInstanceState) {
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
final Context context = getActivity();
mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE;
diff --git a/android/support/v14/preference/PreferenceFragment.java b/android/support/v14/preference/PreferenceFragment.java
index 2421050..406465f 100644
--- a/android/support/v14/preference/PreferenceFragment.java
+++ b/android/support/v14/preference/PreferenceFragment.java
@@ -11,7 +11,7 @@
* 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
+ * limitations under the License.
*/
package android.support.v14.preference;
@@ -44,6 +44,7 @@
import android.support.v7.preference.PreferenceRecyclerViewAccessibilityDelegate;
import android.support.v7.preference.PreferenceScreen;
import android.support.v7.preference.PreferenceViewHolder;
+import android.support.v7.preference.R;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.TypedValue;
@@ -145,7 +146,7 @@
private final DividerDecoration mDividerDecoration = new DividerDecoration();
private static final int MSG_BIND_PREFERENCES = 1;
- private Handler mHandler = new Handler() {
+ private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
@@ -157,7 +158,7 @@
}
};
- final private Runnable mRequestFocus = new Runnable() {
+ private final Runnable mRequestFocus = new Runnable() {
@Override
public void run() {
mList.focusableViewAvailable(mList);
@@ -484,7 +485,7 @@
handled = ((OnPreferenceStartFragmentCallback) getCallbackFragment())
.onPreferenceStartFragment(this, preference);
}
- if (!handled && getActivity() instanceof OnPreferenceStartFragmentCallback){
+ if (!handled && getActivity() instanceof OnPreferenceStartFragmentCallback) {
handled = ((OnPreferenceStartFragmentCallback) getActivity())
.onPreferenceStartFragment(this, preference);
}
@@ -656,8 +657,8 @@
} else if (preference instanceof MultiSelectListPreference) {
f = MultiSelectListPreferenceDialogFragment.newInstance(preference.getKey());
} else {
- throw new IllegalArgumentException("Tried to display dialog for unknown " +
- "preference type. Did you forget to override onDisplayPreferenceDialog()?");
+ throw new IllegalArgumentException("Tried to display dialog for unknown "
+ + "preference type. Did you forget to override onDisplayPreferenceDialog()?");
}
f.setTargetFragment(this, 0);
f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
@@ -686,8 +687,7 @@
@Override
public void run() {
final RecyclerView.Adapter adapter = mList.getAdapter();
- if (!(adapter instanceof
- PreferenceGroup.PreferencePositionCallback)) {
+ if (!(adapter instanceof PreferenceGroup.PreferencePositionCallback)) {
if (adapter != null) {
throw new IllegalStateException("Adapter must implement "
+ "PreferencePositionCallback");
@@ -726,7 +726,7 @@
private final Preference mPreference;
private final String mKey;
- public ScrollToPreferenceObserver(RecyclerView.Adapter adapter, RecyclerView list,
+ ScrollToPreferenceObserver(RecyclerView.Adapter adapter, RecyclerView list,
Preference preference, String key) {
mAdapter = adapter;
mList = list;
diff --git a/android/support/v14/preference/SwitchPreference.java b/android/support/v14/preference/SwitchPreference.java
index eae20b8..197de4e 100644
--- a/android/support/v14/preference/SwitchPreference.java
+++ b/android/support/v14/preference/SwitchPreference.java
@@ -1,18 +1,18 @@
/*
-* Copyright (C) 2015 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License
-*/
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package android.support.v14.preference;
@@ -24,6 +24,7 @@
import android.support.v4.content.res.TypedArrayUtils;
import android.support.v7.preference.AndroidResources;
import android.support.v7.preference.PreferenceViewHolder;
+import android.support.v7.preference.R;
import android.support.v7.preference.TwoStatePreference;
import android.util.AttributeSet;
import android.view.View;
diff --git a/android/support/v17/leanback/app/PlaybackFragment.java b/android/support/v17/leanback/app/PlaybackFragment.java
index e2e6be4..dc59e0e 100644
--- a/android/support/v17/leanback/app/PlaybackFragment.java
+++ b/android/support/v17/leanback/app/PlaybackFragment.java
@@ -29,6 +29,7 @@
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
import android.support.v17.leanback.R;
import android.support.v17.leanback.animation.LogAccelerateInterpolator;
import android.support.v17.leanback.animation.LogDecelerateInterpolator;
@@ -106,6 +107,7 @@
* Resets the focus on the button in the middle of control row.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public void resetFocus() {
ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) getVerticalGridView()
.findViewHolderForAdapterPosition(0);
@@ -185,6 +187,7 @@
* @hide
* @deprecated use {@link PlaybackSupportFragment}
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@Deprecated
public static class OnFadeCompleteListener {
public void onFadeInComplete() {
@@ -366,6 +369,7 @@
* Sets the listener to be called when fade in or out has completed.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public void setFadeCompleteListener(OnFadeCompleteListener listener) {
mFadeCompleteListener = listener;
}
@@ -374,6 +378,7 @@
* Returns the listener to be called when fade in or out has completed.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public OnFadeCompleteListener getFadeCompleteListener() {
return mFadeCompleteListener;
}
diff --git a/android/support/v17/leanback/app/PlaybackSupportFragment.java b/android/support/v17/leanback/app/PlaybackSupportFragment.java
index a8741ab..ee17e84 100644
--- a/android/support/v17/leanback/app/PlaybackSupportFragment.java
+++ b/android/support/v17/leanback/app/PlaybackSupportFragment.java
@@ -26,6 +26,7 @@
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
import android.support.v17.leanback.R;
import android.support.v17.leanback.animation.LogAccelerateInterpolator;
import android.support.v17.leanback.animation.LogDecelerateInterpolator;
@@ -101,6 +102,7 @@
* Resets the focus on the button in the middle of control row.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public void resetFocus() {
ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) getVerticalGridView()
.findViewHolderForAdapterPosition(0);
@@ -179,6 +181,7 @@
* completion events.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public static class OnFadeCompleteListener {
public void onFadeInComplete() {
}
@@ -359,6 +362,7 @@
* Sets the listener to be called when fade in or out has completed.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public void setFadeCompleteListener(OnFadeCompleteListener listener) {
mFadeCompleteListener = listener;
}
@@ -367,6 +371,7 @@
* Returns the listener to be called when fade in or out has completed.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public OnFadeCompleteListener getFadeCompleteListener() {
return mFadeCompleteListener;
}
diff --git a/android/support/v17/leanback/media/PlaybackControlGlue.java b/android/support/v17/leanback/media/PlaybackControlGlue.java
index 5bf6cc1..0a788f6 100644
--- a/android/support/v17/leanback/media/PlaybackControlGlue.java
+++ b/android/support/v17/leanback/media/PlaybackControlGlue.java
@@ -20,6 +20,7 @@
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
+import android.support.annotation.RestrictTo;
import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
import android.support.v17.leanback.widget.Action;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
@@ -356,6 +357,7 @@
/**
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
protected SparseArrayObjectAdapter createPrimaryActionsAdapter(
PresenterSelector presenterSelector) {
SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(presenterSelector);
diff --git a/android/support/v17/leanback/media/PlaybackTransportControlGlue.java b/android/support/v17/leanback/media/PlaybackTransportControlGlue.java
index 4aa9bf6..b81f979 100644
--- a/android/support/v17/leanback/media/PlaybackTransportControlGlue.java
+++ b/android/support/v17/leanback/media/PlaybackTransportControlGlue.java
@@ -365,7 +365,7 @@
@Override
public void onSeekFinished(boolean cancelled) {
if (!cancelled) {
- if (mLastUserPosition > 0) {
+ if (mLastUserPosition >= 0) {
seekTo(mLastUserPosition);
}
} else {
diff --git a/android/support/v17/leanback/transition/TransitionEpicenterCallback.java b/android/support/v17/leanback/transition/TransitionEpicenterCallback.java
index ec7f84c..bb8e686 100644
--- a/android/support/v17/leanback/transition/TransitionEpicenterCallback.java
+++ b/android/support/v17/leanback/transition/TransitionEpicenterCallback.java
@@ -14,11 +14,13 @@
package android.support.v17.leanback.transition;
import android.graphics.Rect;
+import android.support.annotation.RestrictTo;
/**
* Class to get the epicenter of Transition.
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public abstract class TransitionEpicenterCallback {
/**
@@ -31,4 +33,3 @@
*/
public abstract Rect onGetEpicenter(Object transition);
}
-
diff --git a/android/support/v17/leanback/util/MathUtil.java b/android/support/v17/leanback/util/MathUtil.java
index 487188d..bf74e40 100644
--- a/android/support/v17/leanback/util/MathUtil.java
+++ b/android/support/v17/leanback/util/MathUtil.java
@@ -13,10 +13,13 @@
*/
package android.support.v17.leanback.util;
+import android.support.annotation.RestrictTo;
+
/**
* Math Utilities for leanback library.
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public final class MathUtil {
private MathUtil() {
diff --git a/android/support/v17/leanback/widget/DetailsParallaxDrawable.java b/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
index 37e3480..1eea797 100644
--- a/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
+++ b/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
@@ -22,6 +22,7 @@
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.ColorInt;
+import android.support.annotation.RestrictTo;
import android.support.v17.leanback.R;
import android.support.v17.leanback.graphics.CompositeDrawable;
import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
@@ -56,6 +57,7 @@
* </li>
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public class DetailsParallaxDrawable extends CompositeDrawable {
private Drawable mBottomDrawable;
diff --git a/android/support/v17/leanback/widget/GridLayoutManager.java b/android/support/v17/leanback/widget/GridLayoutManager.java
index 613198f..810cb3b 100644
--- a/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -2645,8 +2645,9 @@
mPrimaryScrollExtra = primaryScrollExtra;
View view = findViewByPosition(position);
// scrollToView() is based on Adapter position. Only call scrollToView() when item
- // is still valid.
- if (view != null && getAdapterPositionByView(view) == position) {
+ // is still valid and no layout is requested, otherwise defer to next layout pass.
+ if (!mBaseGridView.isLayoutRequested()
+ && view != null && getAdapterPositionByView(view) == position) {
mFlag |= PF_IN_SELECTION;
scrollToView(view, smooth);
mFlag &= ~PF_IN_SELECTION;
diff --git a/android/support/v17/leanback/widget/MediaRowFocusView.java b/android/support/v17/leanback/widget/MediaRowFocusView.java
index 1418a2a..471f64e 100644
--- a/android/support/v17/leanback/widget/MediaRowFocusView.java
+++ b/android/support/v17/leanback/widget/MediaRowFocusView.java
@@ -17,14 +17,16 @@
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
+import android.support.annotation.RestrictTo;
+import android.support.v17.leanback.R;
import android.util.AttributeSet;
import android.view.View;
-import android.support.v17.leanback.R;
/**
* Creates a view for a media item row in a playlist
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
class MediaRowFocusView extends View {
private final Paint mPaint;
diff --git a/android/support/v17/leanback/widget/ParallaxEffect.java b/android/support/v17/leanback/widget/ParallaxEffect.java
index 5c06e29..e1af762 100644
--- a/android/support/v17/leanback/widget/ParallaxEffect.java
+++ b/android/support/v17/leanback/widget/ParallaxEffect.java
@@ -17,6 +17,7 @@
package android.support.v17.leanback.widget;
import android.animation.PropertyValuesHolder;
+import android.support.annotation.RestrictTo;
import android.support.v17.leanback.widget.Parallax.FloatProperty;
import android.support.v17.leanback.widget.Parallax.FloatPropertyMarkerValue;
import android.support.v17.leanback.widget.Parallax.IntProperty;
@@ -70,6 +71,7 @@
* @return A list of Float objects that represents weight associated with each variable range.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public final List<Float> getWeights() {
return mWeights;
}
@@ -96,6 +98,7 @@
* range.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public final void setWeights(float... weights) {
for (float weight : weights) {
if (weight <= 0) {
@@ -121,6 +124,7 @@
* @return This ParallaxEffect object, allowing calls to methods in this class to be chained.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public final ParallaxEffect weights(float... weights) {
setWeights(weights);
return this;
diff --git a/android/support/v17/leanback/widget/VideoSurfaceView.java b/android/support/v17/leanback/widget/VideoSurfaceView.java
index 29d778c..d42a60d 100644
--- a/android/support/v17/leanback/widget/VideoSurfaceView.java
+++ b/android/support/v17/leanback/widget/VideoSurfaceView.java
@@ -17,6 +17,7 @@
package android.support.v17.leanback.widget;
import android.content.Context;
+import android.support.annotation.RestrictTo;
import android.util.AttributeSet;
import android.view.SurfaceView;
@@ -26,6 +27,7 @@
* This class disables setTransitionVisibility() to avoid the problem.
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public class VideoSurfaceView extends SurfaceView {
public VideoSurfaceView(Context context) {
diff --git a/android/support/v4/app/ActivityCompat.java b/android/support/v4/app/ActivityCompat.java
index 9d15be1..333871a 100644
--- a/android/support/v4/app/ActivityCompat.java
+++ b/android/support/v4/app/ActivityCompat.java
@@ -29,6 +29,7 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Parcelable;
+import android.support.annotation.IdRes;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -340,6 +341,31 @@
}
/**
+ * Finds a view that was identified by the {@code android:id} XML attribute that was processed
+ * in {@link Activity#onCreate}, or throws an IllegalArgumentException if the ID is invalid, or
+ * there is no matching view in the hierarchy.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see Activity#findViewById(int)
+ * @see android.support.v4.view.ViewCompat#requireViewById(View, int)
+ */
+ @NonNull
+ public static <T extends View> T requireViewById(@NonNull Activity activity, @IdRes int id) {
+ // TODO: use and link to Activity#requireViewById() directly, once available
+ T view = activity.findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException("ID does not reference a View inside this Activity");
+ }
+ return view;
+ }
+
+ /**
* When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity,
* android.view.View, String)} was used to start an Activity, <var>callback</var>
* will be called to handle shared elements on the <i>launched</i> Activity. This requires
@@ -538,6 +564,7 @@
* URIs. {@code null} if no content URIs are associated with the event or if permissions could
* not be granted.
*/
+ @Nullable
public static DragAndDropPermissionsCompat requestDragAndDropPermissions(Activity activity,
DragEvent dragEvent) {
return DragAndDropPermissionsCompat.request(activity, dragEvent);
diff --git a/android/support/v4/app/Fragment.java b/android/support/v4/app/Fragment.java
index 5b560cd..f3c73ae 100644
--- a/android/support/v4/app/Fragment.java
+++ b/android/support/v4/app/Fragment.java
@@ -575,7 +575,6 @@
/**
* Return the {@link Context} this fragment is currently associated with.
*/
- @Nullable
public Context getContext() {
return mHost == null ? null : mHost.getContext();
}
@@ -585,7 +584,6 @@
* May return {@code null} if the fragment is associated with a {@link Context}
* instead.
*/
- @Nullable
final public FragmentActivity getActivity() {
return mHost == null ? null : (FragmentActivity) mHost.getActivity();
}
@@ -594,7 +592,6 @@
* Return the host object of this fragment. May return {@code null} if the fragment
* isn't currently being hosted.
*/
- @Nullable
final public Object getHost() {
return mHost == null ? null : mHost.onGetHost();
}
@@ -655,7 +652,6 @@
* <p>If this Fragment is a child of another Fragment, the FragmentManager
* returned here will be the parent's {@link #getChildFragmentManager()}.
*/
- @Nullable
final public FragmentManager getFragmentManager() {
return mFragmentManager;
}
@@ -864,6 +860,12 @@
}
mUserVisibleHint = isVisibleToUser;
mDeferStart = mState < STARTED && !isVisibleToUser;
+ if (mSavedFragmentState != null) {
+ // Ensure that if the user visible hint is set before the Fragment has
+ // restored its state that we don't lose the new value
+ mSavedFragmentState.putBoolean(FragmentManagerImpl.USER_VISIBLE_HINT_TAG,
+ mUserVisibleHint);
+ }
}
/**
diff --git a/android/support/v4/app/FragmentActivity.java b/android/support/v4/app/FragmentActivity.java
index 78161a8..e3f5684 100644
--- a/android/support/v4/app/FragmentActivity.java
+++ b/android/support/v4/app/FragmentActivity.java
@@ -273,6 +273,7 @@
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
+ mFragments.noteStateNotSaved();
mFragments.dispatchConfigurationChanged(newConfig);
}
diff --git a/android/support/v4/app/LoaderManager.java b/android/support/v4/app/LoaderManager.java
index 521b218..32e211a 100644
--- a/android/support/v4/app/LoaderManager.java
+++ b/android/support/v4/app/LoaderManager.java
@@ -16,8 +16,11 @@
package android.support.v4.app;
-import android.app.Activity;
import android.os.Bundle;
+import android.os.Looper;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.v4.content.Loader;
import android.support.v4.util.DebugUtils;
import android.support.v4.util.SparseArrayCompat;
@@ -44,11 +47,15 @@
/**
* Instantiate and return a new Loader for the given ID.
*
+ * <p>This will always be called from the process's main thread.
+ *
* @param id The ID whose loader is to be created.
* @param args Any arguments supplied by the caller.
* @return Return a new Loader instance that is ready to start loading.
*/
- public Loader<D> onCreateLoader(int id, Bundle args);
+ @MainThread
+ @NonNull
+ Loader<D> onCreateLoader(int id, @Nullable Bundle args);
/**
* Called when a previously created loader has finished its load. Note
@@ -86,19 +93,25 @@
* method so that the old Cursor is not closed.
* </ul>
*
+ * <p>This will always be called from the process's main thread.
+ *
* @param loader The Loader that has finished.
* @param data The data generated by the Loader.
*/
- public void onLoadFinished(Loader<D> loader, D data);
+ @MainThread
+ void onLoadFinished(@NonNull Loader<D> loader, D data);
/**
* Called when a previously created loader is being reset, and thus
* making its data unavailable. The application should at this point
* remove any references it has to the Loader's data.
*
+ * <p>This will always be called from the process's main thread.
+ *
* @param loader The Loader that is being reset.
*/
- public void onLoaderReset(Loader<D> loader);
+ @MainThread
+ void onLoaderReset(@NonNull Loader<D> loader);
}
/**
@@ -115,6 +128,8 @@
* be called immediately (inside of this function), so you must be prepared
* for this to happen.
*
+ * <p>Must be called from the process's main thread.
+ *
* @param id A unique identifier for this loader. Can be whatever you want.
* Identifiers are scoped to a particular LoaderManager instance.
* @param args Optional arguments to supply to the loader at construction.
@@ -123,8 +138,10 @@
* @param callback Interface the LoaderManager will call to report about
* changes in the state of the loader. Required.
*/
- public abstract <D> Loader<D> initLoader(int id, Bundle args,
- LoaderManager.LoaderCallbacks<D> callback);
+ @MainThread
+ @NonNull
+ public abstract <D> Loader<D> initLoader(int id, @Nullable Bundle args,
+ @NonNull LoaderManager.LoaderCallbacks<D> callback);
/**
* Starts a new or restarts an existing {@link android.content.Loader} in
@@ -135,27 +152,35 @@
* its work. The callback will be delivered before the old loader
* is destroyed.
*
+ * <p>Must be called from the process's main thread.
+ *
* @param id A unique identifier for this loader. Can be whatever you want.
* Identifiers are scoped to a particular LoaderManager instance.
* @param args Optional arguments to supply to the loader at construction.
* @param callback Interface the LoaderManager will call to report about
* changes in the state of the loader. Required.
*/
- public abstract <D> Loader<D> restartLoader(int id, Bundle args,
- LoaderManager.LoaderCallbacks<D> callback);
+ @MainThread
+ @NonNull
+ public abstract <D> Loader<D> restartLoader(int id, @Nullable Bundle args,
+ @NonNull LoaderManager.LoaderCallbacks<D> callback);
/**
* Stops and removes the loader with the given ID. If this loader
* had previously reported data to the client through
* {@link LoaderCallbacks#onLoadFinished(Loader, Object)}, a call
* will be made to {@link LoaderCallbacks#onLoaderReset(Loader)}.
+ *
+ * <p>Must be called from the process's main thread.
*/
+ @MainThread
public abstract void destroyLoader(int id);
/**
* Return the Loader with the given id or null if no matching Loader
* is found.
*/
+ @Nullable
public abstract <D> Loader<D> getLoader(int id);
/**
@@ -378,7 +403,7 @@
}
@Override
- public void onLoadCanceled(Loader<Object> loader) {
+ public void onLoadCanceled(@NonNull Loader<Object> loader) {
if (DEBUG) Log.v(TAG, "onLoadCanceled: " + this);
if (mDestroyed) {
@@ -407,7 +432,7 @@
}
@Override
- public void onLoadComplete(Loader<Object> loader, Object data) {
+ public void onLoadComplete(@NonNull Loader<Object> loader, Object data) {
if (DEBUG) Log.v(TAG, "onLoadComplete: " + this);
if (mDestroyed) {
@@ -563,36 +588,18 @@
}
}
- /**
- * Call to initialize a particular ID with a Loader. If this ID already
- * has a Loader associated with it, it is left unchanged and any previous
- * callbacks replaced with the newly provided ones. If there is not currently
- * a Loader for the ID, a new one is created and started.
- *
- * <p>This function should generally be used when a component is initializing,
- * to ensure that a Loader it relies on is created. This allows it to re-use
- * an existing Loader's data if there already is one, so that for example
- * when an {@link Activity} is re-created after a configuration change it
- * does not need to re-create its loaders.
- *
- * <p>Note that in the case where an existing Loader is re-used, the
- * <var>args</var> given here <em>will be ignored</em> because you will
- * continue using the previous Loader.
- *
- * @param id A unique (to this LoaderManager instance) identifier under
- * which to manage the new Loader.
- * @param args Optional arguments that will be propagated to
- * {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onCreateLoader(int, Bundle) LoaderCallbacks.onCreateLoader()}.
- * @param callback Interface implementing management of this Loader. Required.
- * Its onCreateLoader() method will be called while inside of the function to
- * instantiate the Loader object.
- */
+ @MainThread
+ @NonNull
@Override
@SuppressWarnings("unchecked")
- public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
+ public <D> Loader<D> initLoader(int id, @Nullable Bundle args,
+ @NonNull LoaderManager.LoaderCallbacks<D> callback) {
if (mCreatingLoader) {
throw new IllegalStateException("Called while creating a loader");
}
+ if (Looper.getMainLooper() != Looper.myLooper()) {
+ throw new IllegalStateException("initLoader must be called on the main thread");
+ }
LoaderInfo info = mLoaders.get(id);
@@ -615,35 +622,18 @@
return (Loader<D>)info.mLoader;
}
- /**
- * Call to re-create the Loader associated with a particular ID. If there
- * is currently a Loader associated with this ID, it will be
- * canceled/stopped/destroyed as appropriate. A new Loader with the given
- * arguments will be created and its data delivered to you once available.
- *
- * <p>This function does some throttling of Loaders. If too many Loaders
- * have been created for the given ID but not yet generated their data,
- * new calls to this function will create and return a new Loader but not
- * actually start it until some previous loaders have completed.
- *
- * <p>After calling this function, any previous Loaders associated with
- * this ID will be considered invalid, and you will receive no further
- * data updates from them.
- *
- * @param id A unique (to this LoaderManager instance) identifier under
- * which to manage the new Loader.
- * @param args Optional arguments that will be propagated to
- * {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onCreateLoader(int, Bundle) LoaderCallbacks.onCreateLoader()}.
- * @param callback Interface implementing management of this Loader. Required.
- * Its onCreateLoader() method will be called while inside of the function to
- * instantiate the Loader object.
- */
+ @MainThread
+ @NonNull
@Override
@SuppressWarnings("unchecked")
- public <D> Loader<D> restartLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
+ public <D> Loader<D> restartLoader(int id, @Nullable Bundle args,
+ @NonNull LoaderManager.LoaderCallbacks<D> callback) {
if (mCreatingLoader) {
throw new IllegalStateException("Called while creating a loader");
}
+ if (Looper.getMainLooper() != Looper.myLooper()) {
+ throw new IllegalStateException("restartLoader must be called on the main thread");
+ }
LoaderInfo info = mLoaders.get(id);
if (DEBUG) Log.v(TAG, "restartLoader in " + this + ": args=" + args);
@@ -701,18 +691,15 @@
return (Loader<D>)info.mLoader;
}
- /**
- * Rip down, tear apart, shred to pieces a current Loader ID. After returning
- * from this function, any Loader objects associated with this ID are
- * destroyed. Any data associated with them is destroyed. You better not
- * be using it when you do this.
- * @param id Identifier of the Loader to be destroyed.
- */
+ @MainThread
@Override
public void destroyLoader(int id) {
if (mCreatingLoader) {
throw new IllegalStateException("Called while creating a loader");
}
+ if (Looper.getMainLooper() != Looper.myLooper()) {
+ throw new IllegalStateException("destroyLoader must be called on the main thread");
+ }
if (DEBUG) Log.v(TAG, "destroyLoader in " + this + " of " + id);
int idx = mLoaders.indexOfKey(id);
@@ -732,10 +719,7 @@
}
}
- /**
- * Return the most recent Loader object associated with the
- * given ID.
- */
+ @Nullable
@Override
@SuppressWarnings("unchecked")
public <D> Loader<D> getLoader(int id) {
diff --git a/android/support/v4/app/NotificationCompat.java b/android/support/v4/app/NotificationCompat.java
index 6f74e18..9d71ad1 100644
--- a/android/support/v4/app/NotificationCompat.java
+++ b/android/support/v4/app/NotificationCompat.java
@@ -453,6 +453,7 @@
public @interface StreamType {}
/** @hide */
+ @RestrictTo(LIBRARY_GROUP)
@Retention(SOURCE)
@IntDef({VISIBILITY_PUBLIC, VISIBILITY_PRIVATE, VISIBILITY_SECRET})
public @interface NotificationVisibility {}
@@ -2114,8 +2115,16 @@
/**
* Sets the title to be displayed on this conversation. May be set to {@code null}.
- * @param conversationTitle Title displayed for this conversation.
- * @return this object for method chaining.
+ *
+ * <p>This API's behavior was changed in SDK version {@link Build.VERSION_CODES#P}. If your
+ * application's target version is less than {@link Build.VERSION_CODES#P}, setting a
+ * conversation title to a non-null value will make {@link #isGroupConversation()} return
+ * {@code true} and passing {@code null} will make it return {@code false}. In
+ * {@link Build.VERSION_CODES#P} and beyond, use {@link #setGroupConversation(boolean)}
+ * to set group conversation status.
+ *
+ * @param conversationTitle Title displayed for this conversation
+ * @return this object for method chaining
*/
public MessagingStyle setConversationTitle(@Nullable CharSequence conversationTitle) {
mConversationTitle = conversationTitle;
@@ -2185,9 +2194,27 @@
}
/**
- * Returns {@code true} if this notification represents a group conversation.
+ * Returns {@code true} if this notification represents a group conversation, otherwise
+ * {@code false}.
+ *
+ * <p> If the application that generated this {@link MessagingStyle} targets an SDK version
+ * less than {@link Build.VERSION_CODES#P}, this method becomes dependent on whether or
+ * not the conversation title is set; returning {@code true} if the conversation title is
+ * a non-null value, or {@code false} otherwise. From {@link Build.VERSION_CODES#P} forward,
+ * this method returns what's set by {@link #setGroupConversation(boolean)} allowing for
+ * named, non-group conversations.
+ *
+ * @see #setConversationTitle(CharSequence)
*/
public boolean isGroupConversation() {
+ // When target SDK version is < P, a non-null conversation title dictates if this is
+ // as group conversation.
+ if (mBuilder != null
+ && mBuilder.mContext.getApplicationInfo().targetSdkVersion
+ < Build.VERSION_CODES.P) {
+ return mConversationTitle != null;
+ }
+
return mIsGroupConversation;
}
@@ -2769,6 +2796,66 @@
* to attach actions.
*/
public static class Action {
+ /**
+ * {@link SemanticAction}: No semantic action defined.
+ */
+ public static final int SEMANTIC_ACTION_NONE = 0;
+
+ /**
+ * {@link SemanticAction}: Reply to a conversation, chat, group, or wherever replies
+ * may be appropriate.
+ */
+ public static final int SEMANTIC_ACTION_REPLY = 1;
+
+ /**
+ * {@link SemanticAction}: Mark content as read.
+ */
+ public static final int SEMANTIC_ACTION_MARK_AS_READ = 2;
+
+ /**
+ * {@link SemanticAction}: Mark content as unread.
+ */
+ public static final int SEMANTIC_ACTION_MARK_AS_UNREAD = 3;
+
+ /**
+ * {@link SemanticAction}: Delete the content associated with the notification. This
+ * could mean deleting an email, message, etc.
+ */
+ public static final int SEMANTIC_ACTION_DELETE = 4;
+
+ /**
+ * {@link SemanticAction}: Archive the content associated with the notification. This
+ * could mean archiving an email, message, etc.
+ */
+ public static final int SEMANTIC_ACTION_ARCHIVE = 5;
+
+ /**
+ * {@link SemanticAction}: Mute the content associated with the notification. This could
+ * mean silencing a conversation or currently playing media.
+ */
+ public static final int SEMANTIC_ACTION_MUTE = 6;
+
+ /**
+ * {@link SemanticAction}: Unmute the content associated with the notification. This could
+ * mean un-silencing a conversation or currently playing media.
+ */
+ public static final int SEMANTIC_ACTION_UNMUTE = 7;
+
+ /**
+ * {@link SemanticAction}: Mark content with a thumbs up.
+ */
+ public static final int SEMANTIC_ACTION_THUMBS_UP = 8;
+
+ /**
+ * {@link SemanticAction}: Mark content with a thumbs down.
+ */
+ public static final int SEMANTIC_ACTION_THUMBS_DOWN = 9;
+
+ static final String EXTRA_SHOWS_USER_INTERFACE =
+ "android.support.action.showsUserInterface";
+
+ static final String EXTRA_SEMANTIC_ACTION = "android.support.action.semanticAction";
+
final Bundle mExtras;
private final RemoteInput[] mRemoteInputs;
@@ -2785,6 +2872,9 @@
private final RemoteInput[] mDataOnlyRemoteInputs;
private boolean mAllowGeneratedReplies;
+ private boolean mShowsUserInterface = true;
+
+ private final @SemanticAction int mSemanticAction;
/**
* Small icon representing the action.
@@ -2801,12 +2891,13 @@
public PendingIntent actionIntent;
public Action(int icon, CharSequence title, PendingIntent intent) {
- this(icon, title, intent, new Bundle(), null, null, true);
+ this(icon, title, intent, new Bundle(), null, null, true, SEMANTIC_ACTION_NONE, true);
}
Action(int icon, CharSequence title, PendingIntent intent, Bundle extras,
RemoteInput[] remoteInputs, RemoteInput[] dataOnlyRemoteInputs,
- boolean allowGeneratedReplies) {
+ boolean allowGeneratedReplies, @SemanticAction int semanticAction,
+ boolean showsUserInterface) {
this.icon = icon;
this.title = NotificationCompat.Builder.limitCharSequenceLength(title);
this.actionIntent = intent;
@@ -2814,6 +2905,8 @@
this.mRemoteInputs = remoteInputs;
this.mDataOnlyRemoteInputs = dataOnlyRemoteInputs;
this.mAllowGeneratedReplies = allowGeneratedReplies;
+ this.mSemanticAction = semanticAction;
+ this.mShowsUserInterface = showsUserInterface;
}
public int getIcon() {
@@ -2853,6 +2946,17 @@
}
/**
+ * Returns the {@link SemanticAction} associated with this {@link Action}. A
+ * {@link SemanticAction} denotes what an {@link Action}'s {@link PendingIntent} will do
+ * (eg. reply, mark as read, delete, etc).
+ *
+ * @see SemanticAction
+ */
+ public @SemanticAction int getSemanticAction() {
+ return mSemanticAction;
+ }
+
+ /**
* Get the list of inputs to be collected from the user that ONLY accept data when this
* action is sent. These remote inputs are guaranteed to return true on a call to
* {@link RemoteInput#isDataOnly}.
@@ -2867,6 +2971,14 @@
}
/**
+ * Return whether or not triggering this {@link Action}'s {@link PendingIntent} will open a
+ * user interface.
+ */
+ public boolean getShowsUserInterface() {
+ return mShowsUserInterface;
+ }
+
+ /**
* Builder class for {@link Action} objects.
*/
public static final class Builder {
@@ -2876,6 +2988,8 @@
private boolean mAllowGeneratedReplies = true;
private final Bundle mExtras;
private ArrayList<RemoteInput> mRemoteInputs;
+ private @SemanticAction int mSemanticAction;
+ private boolean mShowsUserInterface = true;
/**
* Construct a new builder for {@link Action} object.
@@ -2884,7 +2998,7 @@
* @param intent the {@link PendingIntent} to fire when users trigger this action
*/
public Builder(int icon, CharSequence title, PendingIntent intent) {
- this(icon, title, intent, new Bundle(), null, true);
+ this(icon, title, intent, new Bundle(), null, true, SEMANTIC_ACTION_NONE, true);
}
/**
@@ -2894,11 +3008,13 @@
*/
public Builder(Action action) {
this(action.icon, action.title, action.actionIntent, new Bundle(action.mExtras),
- action.getRemoteInputs(), action.getAllowGeneratedReplies());
+ action.getRemoteInputs(), action.getAllowGeneratedReplies(),
+ action.getSemanticAction(), action.mShowsUserInterface);
}
private Builder(int icon, CharSequence title, PendingIntent intent, Bundle extras,
- RemoteInput[] remoteInputs, boolean allowGeneratedReplies) {
+ RemoteInput[] remoteInputs, boolean allowGeneratedReplies,
+ @SemanticAction int semanticAction, boolean showsUserInterface) {
mIcon = icon;
mTitle = NotificationCompat.Builder.limitCharSequenceLength(title);
mIntent = intent;
@@ -2906,6 +3022,8 @@
mRemoteInputs = remoteInputs == null ? null : new ArrayList<>(
Arrays.asList(remoteInputs));
mAllowGeneratedReplies = allowGeneratedReplies;
+ mSemanticAction = semanticAction;
+ mShowsUserInterface = showsUserInterface;
}
/**
@@ -2961,6 +3079,32 @@
}
/**
+ * Sets the {@link SemanticAction} for this {@link Action}. A {@link SemanticAction}
+ * denotes what an {@link Action}'s {@link PendingIntent} will do (eg. reply, mark
+ * as read, delete, etc).
+ * @param semanticAction a {@link SemanticAction} defined within {@link Action} with
+ * {@code SEMANTIC_ACTION_} prefixes
+ * @return this object for method chaining
+ */
+ public Builder setSemanticAction(@SemanticAction int semanticAction) {
+ mSemanticAction = semanticAction;
+ return this;
+ }
+
+ /**
+ * Set whether or not this {@link Action}'s {@link PendingIntent} will open a user
+ * interface.
+ * @param showsUserInterface {@code true} if this {@link Action}'s {@link PendingIntent}
+ * will open a user interface, otherwise {@code false}
+ * @return this object for method chaining
+ * The default value is {@code true}
+ */
+ public Builder setShowsUserInterface(boolean showsUserInterface) {
+ mShowsUserInterface = showsUserInterface;
+ return this;
+ }
+
+ /**
* Apply an extender to this action builder. Extenders may be used to add
* metadata or change options on this builder.
*/
@@ -2991,7 +3135,8 @@
RemoteInput[] textInputsArr = textInputs.isEmpty()
? null : textInputs.toArray(new RemoteInput[textInputs.size()]);
return new Action(mIcon, mTitle, mIntent, mExtras, textInputsArr,
- dataOnlyInputsArr, mAllowGeneratedReplies);
+ dataOnlyInputsArr, mAllowGeneratedReplies, mSemanticAction,
+ mShowsUserInterface);
}
}
@@ -3251,6 +3396,27 @@
return (mFlags & FLAG_HINT_DISPLAY_INLINE) != 0;
}
}
+
+ /**
+ * Provides meaning to an {@link Action} that hints at what the associated
+ * {@link PendingIntent} will do. For example, an {@link Action} with a
+ * {@link PendingIntent} that replies to a text message notification may have the
+ * {@link #SEMANTIC_ACTION_REPLY} {@link SemanticAction} set within it.
+ */
+ @IntDef({
+ SEMANTIC_ACTION_NONE,
+ SEMANTIC_ACTION_REPLY,
+ SEMANTIC_ACTION_MARK_AS_READ,
+ SEMANTIC_ACTION_MARK_AS_UNREAD,
+ SEMANTIC_ACTION_DELETE,
+ SEMANTIC_ACTION_ARCHIVE,
+ SEMANTIC_ACTION_MUTE,
+ SEMANTIC_ACTION_UNMUTE,
+ SEMANTIC_ACTION_THUMBS_UP,
+ SEMANTIC_ACTION_THUMBS_DOWN
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SemanticAction {}
}
@@ -4651,8 +4817,21 @@
allowGeneratedReplies = action.getExtras().getBoolean(
NotificationCompatJellybean.EXTRA_ALLOW_GENERATED_REPLIES);
}
+
+ final boolean showsUserInterface =
+ action.getExtras().getBoolean(Action.EXTRA_SHOWS_USER_INTERFACE, true);
+
+ final @Action.SemanticAction int semanticAction;
+ if (Build.VERSION.SDK_INT >= 28) {
+ semanticAction = action.getSemanticAction();
+ } else {
+ semanticAction = action.getExtras().getInt(
+ Action.EXTRA_SEMANTIC_ACTION, Action.SEMANTIC_ACTION_NONE);
+ }
+
return new Action(action.icon, action.title, action.actionIntent,
- action.getExtras(), remoteInputs, null, allowGeneratedReplies);
+ action.getExtras(), remoteInputs, null, allowGeneratedReplies,
+ semanticAction, showsUserInterface);
}
/**
diff --git a/android/support/v4/app/NotificationCompatBuilder.java b/android/support/v4/app/NotificationCompatBuilder.java
index db775a5..e5fb4f9 100644
--- a/android/support/v4/app/NotificationCompatBuilder.java
+++ b/android/support/v4/app/NotificationCompatBuilder.java
@@ -248,6 +248,15 @@
if (Build.VERSION.SDK_INT >= 24) {
actionBuilder.setAllowGeneratedReplies(action.getAllowGeneratedReplies());
}
+
+ actionExtras.putInt(NotificationCompat.Action.EXTRA_SEMANTIC_ACTION,
+ action.getSemanticAction());
+ if (Build.VERSION.SDK_INT >= 28) {
+ actionBuilder.setSemanticAction(action.getSemanticAction());
+ }
+
+ actionExtras.putBoolean(NotificationCompat.Action.EXTRA_SHOWS_USER_INTERFACE,
+ action.getShowsUserInterface());
actionBuilder.addExtras(actionExtras);
mBuilder.addAction(actionBuilder.build());
} else if (Build.VERSION.SDK_INT >= 16) {
diff --git a/android/support/v4/app/NotificationCompatJellybean.java b/android/support/v4/app/NotificationCompatJellybean.java
index 9cdd2e9..82f8941 100644
--- a/android/support/v4/app/NotificationCompatJellybean.java
+++ b/android/support/v4/app/NotificationCompatJellybean.java
@@ -129,7 +129,8 @@
allowGeneratedReplies = extras.getBoolean(EXTRA_ALLOW_GENERATED_REPLIES);
}
return new NotificationCompat.Action(icon, title, actionIntent, extras, remoteInputs,
- dataOnlyRemoteInputs, allowGeneratedReplies);
+ dataOnlyRemoteInputs, allowGeneratedReplies,
+ NotificationCompat.Action.SEMANTIC_ACTION_NONE, true);
}
public static Bundle writeActionAndGetExtras(
@@ -236,7 +237,9 @@
bundle.getBundle(KEY_EXTRAS),
fromBundleArray(getBundleArrayFromBundle(bundle, KEY_REMOTE_INPUTS)),
fromBundleArray(getBundleArrayFromBundle(bundle, KEY_DATA_ONLY_REMOTE_INPUTS)),
- allowGeneratedReplies);
+ allowGeneratedReplies,
+ NotificationCompat.Action.SEMANTIC_ACTION_NONE,
+ true);
}
static Bundle getBundleForAction(NotificationCompat.Action action) {
diff --git a/android/support/v4/app/NotificationManagerCompat.java b/android/support/v4/app/NotificationManagerCompat.java
index 07fcb6c..7099cb9 100644
--- a/android/support/v4/app/NotificationManagerCompat.java
+++ b/android/support/v4/app/NotificationManagerCompat.java
@@ -190,7 +190,7 @@
* @param id the ID of the notification
* @param notification the notification to post to the system
*/
- public void notify(int id, Notification notification) {
+ public void notify(int id, @NonNull Notification notification) {
notify(null, id, notification);
}
diff --git a/android/support/v4/content/FileProvider.java b/android/support/v4/content/FileProvider.java
index 8599911..16164be 100644
--- a/android/support/v4/content/FileProvider.java
+++ b/android/support/v4/content/FileProvider.java
@@ -29,6 +29,7 @@
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
+import android.os.Build;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
@@ -177,6 +178,17 @@
* subdirectory is the same as the value returned by
* {@link Context#getExternalCacheDir() Context.getExternalCacheDir()}.
* </dd>
+ * <dt>
+ * <pre class="prettyprint">
+ *<external-media-path name="<i>name</i>" path="<i>path</i>" />
+ *</pre>
+ * </dt>
+ * <dd>
+ * Represents files in the root of your app's external media area. The root path of this
+ * subdirectory is the same as the value returned by the first result of
+ * {@link Context#getExternalMediaDirs() Context.getExternalMediaDirs()}.
+ * <p><strong>Note:</strong> this directory is only available on API 21+ devices.</p>
+ * </dd>
* </dl>
* <p>
* These child elements all use the same attributes:
@@ -336,6 +348,7 @@
private static final String TAG_EXTERNAL = "external-path";
private static final String TAG_EXTERNAL_FILES = "external-files-path";
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
+ private static final String TAG_EXTERNAL_MEDIA = "external-media-path";
private static final String ATTR_NAME = "name";
private static final String ATTR_PATH = "path";
@@ -622,6 +635,12 @@
if (externalCacheDirs.length > 0) {
target = externalCacheDirs[0];
}
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
+ && TAG_EXTERNAL_MEDIA.equals(tag)) {
+ File[] externalMediaDirs = context.getExternalMediaDirs();
+ if (externalMediaDirs.length > 0) {
+ target = externalMediaDirs[0];
+ }
}
if (target != null) {
diff --git a/android/support/v4/content/Loader.java b/android/support/v4/content/Loader.java
index 2ac10d7..431964d 100644
--- a/android/support/v4/content/Loader.java
+++ b/android/support/v4/content/Loader.java
@@ -19,6 +19,7 @@
import android.content.Context;
import android.database.ContentObserver;
import android.os.Handler;
+import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.DebugUtils;
@@ -123,6 +124,7 @@
*
* @param data the result of the load
*/
+ @MainThread
public void deliverResult(@Nullable D data) {
if (mListener != null) {
mListener.onLoadComplete(this, data);
@@ -135,6 +137,7 @@
*
* Must be called from the process's main thread.
*/
+ @MainThread
public void deliverCancellation() {
if (mOnLoadCanceledListener != null) {
mOnLoadCanceledListener.onLoadCanceled(this);
@@ -163,6 +166,7 @@
*
* <p>Must be called from the process's main thread.
*/
+ @MainThread
public void registerListener(int id, @NonNull OnLoadCompleteListener<D> listener) {
if (mListener != null) {
throw new IllegalStateException("There is already a listener registered");
@@ -176,6 +180,7 @@
*
* Must be called from the process's main thread.
*/
+ @MainThread
public void unregisterListener(@NonNull OnLoadCompleteListener<D> listener) {
if (mListener == null) {
throw new IllegalStateException("No listener register");
@@ -195,6 +200,7 @@
*
* @param listener The listener to register.
*/
+ @MainThread
public void registerOnLoadCanceledListener(@NonNull OnLoadCanceledListener<D> listener) {
if (mOnLoadCanceledListener != null) {
throw new IllegalStateException("There is already a listener registered");
@@ -210,6 +216,7 @@
*
* @param listener The listener to unregister.
*/
+ @MainThread
public void unregisterOnLoadCanceledListener(@NonNull OnLoadCanceledListener<D> listener) {
if (mOnLoadCanceledListener == null) {
throw new IllegalStateException("No listener register");
@@ -268,6 +275,7 @@
*
* <p>Must be called from the process's main thread.
*/
+ @MainThread
public final void startLoading() {
mStarted = true;
mReset = false;
@@ -279,7 +287,9 @@
* Subclasses must implement this to take care of loading their data,
* as per {@link #startLoading()}. This is not called by clients directly,
* but as a result of a call to {@link #startLoading()}.
+ * This will always be called from the process's main thread.
*/
+ @MainThread
protected void onStartLoading() {
}
@@ -301,6 +311,7 @@
* is still running and the {@link OnLoadCanceledListener} will be called
* when the task completes.
*/
+ @MainThread
public boolean cancelLoad() {
return onCancelLoad();
}
@@ -316,6 +327,7 @@
* is still running and the {@link OnLoadCanceledListener} will be called
* when the task completes.
*/
+ @MainThread
protected boolean onCancelLoad() {
return false;
}
@@ -328,6 +340,7 @@
*
* <p>Must be called from the process's main thread.
*/
+ @MainThread
public void forceLoad() {
onForceLoad();
}
@@ -336,6 +349,7 @@
* Subclasses must implement this to take care of requests to {@link #forceLoad()}.
* This will always be called from the process's main thread.
*/
+ @MainThread
protected void onForceLoad() {
}
@@ -359,6 +373,7 @@
*
* <p>Must be called from the process's main thread.
*/
+ @MainThread
public void stopLoading() {
mStarted = false;
onStopLoading();
@@ -370,6 +385,7 @@
* but as a result of a call to {@link #stopLoading()}.
* This will always be called from the process's main thread.
*/
+ @MainThread
protected void onStopLoading() {
}
@@ -383,12 +399,15 @@
* Tell the Loader that it is being abandoned. This is called prior
* to {@link #reset} to have it retain its current data but not report
* any new data.
+ *
+ * <p>Must be called from the process's main thread.
*/
+ @MainThread
public void abandon() {
mAbandoned = true;
onAbandon();
}
-
+
/**
* Subclasses implement this to take care of being abandoned. This is
* an optional intermediate state prior to {@link #onReset()} -- it means that
@@ -397,10 +416,12 @@
* loader <em>must</em> keep its last reported data valid until the final
* {@link #onReset()} happens. You can retrieve the current abandoned
* state with {@link #isAbandoned}.
+ * This will always be called from the process's main thread.
*/
+ @MainThread
protected void onAbandon() {
}
-
+
/**
* This function will normally be called for you automatically by
* {@link android.support.v4.app.LoaderManager} when destroying a Loader. When using
@@ -419,6 +440,7 @@
*
* <p>Must be called from the process's main thread.
*/
+ @MainThread
public void reset() {
onReset();
mReset = true;
@@ -434,6 +456,7 @@
* but as a result of a call to {@link #reset()}.
* This will always be called from the process's main thread.
*/
+ @MainThread
protected void onReset() {
}
@@ -481,6 +504,7 @@
*
* <p>Must be called from the process's main thread.
*/
+ @MainThread
public void onContentChanged() {
if (mStarted) {
forceLoad();
diff --git a/android/support/v4/content/WakefulBroadcastReceiver.java b/android/support/v4/content/WakefulBroadcastReceiver.java
index 8ec3eee..78555aa 100644
--- a/android/support/v4/content/WakefulBroadcastReceiver.java
+++ b/android/support/v4/content/WakefulBroadcastReceiver.java
@@ -34,6 +34,9 @@
* for you; you must request the {@link android.Manifest.permission#WAKE_LOCK}
* permission to use it.</p>
*
+ * <p>Wakelocks held by this class are reported to tools as
+ * {@code "androidx.core:wake:<component-name>"}.</p>
+ *
* <h3>Example</h3>
*
* <p>A {@link WakefulBroadcastReceiver} uses the method
@@ -103,7 +106,7 @@
PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
- "wake:" + comp.flattenToShortString());
+ "androidx.core:wake:" + comp.flattenToShortString());
wl.setReferenceCounted(false);
wl.acquire(60 * 1000);
sActiveWakeLocks.put(id, wl);
diff --git a/android/support/v4/graphics/TypefaceCompatUtil.java b/android/support/v4/graphics/TypefaceCompatUtil.java
index b5d206c..c524f82 100644
--- a/android/support/v4/graphics/TypefaceCompatUtil.java
+++ b/android/support/v4/graphics/TypefaceCompatUtil.java
@@ -94,11 +94,15 @@
@RequiresApi(19)
public static ByteBuffer mmap(Context context, CancellationSignal cancellationSignal, Uri uri) {
final ContentResolver resolver = context.getContentResolver();
- try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r", cancellationSignal);
- FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) {
- FileChannel channel = fis.getChannel();
- final long size = channel.size();
- return channel.map(FileChannel.MapMode.READ_ONLY, 0, size);
+ try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r", cancellationSignal)) {
+ if (pfd == null) {
+ return null;
+ }
+ try (FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) {
+ FileChannel channel = fis.getChannel();
+ final long size = channel.size();
+ return channel.map(FileChannel.MapMode.READ_ONLY, 0, size);
+ }
} catch (IOException e) {
return null;
}
diff --git a/android/support/v4/graphics/drawable/DrawableCompat.java b/android/support/v4/graphics/drawable/DrawableCompat.java
index 4e988ea..f15354e 100644
--- a/android/support/v4/graphics/drawable/DrawableCompat.java
+++ b/android/support/v4/graphics/drawable/DrawableCompat.java
@@ -229,8 +229,8 @@
// children manually
if (drawable instanceof InsetDrawable) {
clearColorFilter(((InsetDrawable) drawable).getDrawable());
- } else if (drawable instanceof DrawableWrapper) {
- clearColorFilter(((DrawableWrapper) drawable).getWrappedDrawable());
+ } else if (drawable instanceof WrappedDrawable) {
+ clearColorFilter(((WrappedDrawable) drawable).getWrappedDrawable());
} else if (drawable instanceof DrawableContainer) {
final DrawableContainer container = (DrawableContainer) drawable;
final DrawableContainer.DrawableContainerState state =
@@ -307,17 +307,17 @@
return drawable;
} else if (Build.VERSION.SDK_INT >= 21) {
if (!(drawable instanceof TintAwareDrawable)) {
- return new DrawableWrapperApi21(drawable);
+ return new WrappedDrawableApi21(drawable);
}
return drawable;
} else if (Build.VERSION.SDK_INT >= 19) {
if (!(drawable instanceof TintAwareDrawable)) {
- return new DrawableWrapperApi19(drawable);
+ return new WrappedDrawableApi19(drawable);
}
return drawable;
} else {
if (!(drawable instanceof TintAwareDrawable)) {
- return new DrawableWrapperApi14(drawable);
+ return new WrappedDrawableApi14(drawable);
}
return drawable;
}
@@ -335,8 +335,8 @@
*/
@SuppressWarnings("TypeParameterUnusedInFormals")
public static <T extends Drawable> T unwrap(@NonNull Drawable drawable) {
- if (drawable instanceof DrawableWrapper) {
- return (T) ((DrawableWrapper) drawable).getWrappedDrawable();
+ if (drawable instanceof WrappedDrawable) {
+ return (T) ((WrappedDrawable) drawable).getWrappedDrawable();
}
return (T) drawable;
}
diff --git a/android/support/v4/graphics/drawable/DrawableWrapper.java b/android/support/v4/graphics/drawable/WrappedDrawable.java
similarity index 96%
rename from android/support/v4/graphics/drawable/DrawableWrapper.java
rename to android/support/v4/graphics/drawable/WrappedDrawable.java
index 1574b36..3bd1d68 100644
--- a/android/support/v4/graphics/drawable/DrawableWrapper.java
+++ b/android/support/v4/graphics/drawable/WrappedDrawable.java
@@ -28,7 +28,7 @@
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
-public interface DrawableWrapper {
+public interface WrappedDrawable {
Drawable getWrappedDrawable();
void setWrappedDrawable(Drawable drawable);
}
diff --git a/android/support/v4/graphics/drawable/DrawableWrapperApi14.java b/android/support/v4/graphics/drawable/WrappedDrawableApi14.java
similarity index 89%
rename from android/support/v4/graphics/drawable/DrawableWrapperApi14.java
rename to android/support/v4/graphics/drawable/WrappedDrawableApi14.java
index 5b1bbc7..d1218bc 100644
--- a/android/support/v4/graphics/drawable/DrawableWrapperApi14.java
+++ b/android/support/v4/graphics/drawable/WrappedDrawableApi14.java
@@ -26,7 +26,6 @@
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.support.annotation.RequiresApi;
/**
* Drawable which delegates all calls to its wrapped {@link Drawable}.
@@ -34,10 +33,8 @@
* Also allows backward compatible tinting via a color or {@link ColorStateList}.
* This functionality is accessed via static methods in {@code DrawableCompat}.
*/
-
-@RequiresApi(14)
-class DrawableWrapperApi14 extends Drawable
- implements Drawable.Callback, DrawableWrapper, TintAwareDrawable {
+class WrappedDrawableApi14 extends Drawable
+ implements Drawable.Callback, WrappedDrawable, TintAwareDrawable {
static final PorterDuff.Mode DEFAULT_TINT_MODE = PorterDuff.Mode.SRC_IN;
@@ -50,7 +47,7 @@
Drawable mDrawable;
- DrawableWrapperApi14(@NonNull DrawableWrapperState state, @Nullable Resources res) {
+ WrappedDrawableApi14(@NonNull DrawableWrapperState state, @Nullable Resources res) {
mState = state;
updateLocalState(res);
}
@@ -60,7 +57,7 @@
*
* @param dr the drawable to wrap
*/
- DrawableWrapperApi14(@Nullable Drawable dr) {
+ WrappedDrawableApi14(@Nullable Drawable dr) {
mState = mutateConstantState();
// Now set the drawable...
setWrappedDrawable(dr);
@@ -73,26 +70,17 @@
*/
private void updateLocalState(@Nullable Resources res) {
if (mState != null && mState.mDrawableState != null) {
- final Drawable dr = newDrawableFromState(mState.mDrawableState, res);
- setWrappedDrawable(dr);
+ setWrappedDrawable(mState.mDrawableState.newDrawable(res));
}
}
- /**
- * Allows us to call ConstantState.newDrawable(*) is a API safe way
- */
- protected Drawable newDrawableFromState(@NonNull Drawable.ConstantState state,
- @Nullable Resources res) {
- return state.newDrawable(res);
- }
-
@Override
public void jumpToCurrentState() {
mDrawable.jumpToCurrentState();
}
@Override
- public void draw(Canvas canvas) {
+ public void draw(@NonNull Canvas canvas) {
mDrawable.draw(canvas);
}
@@ -144,17 +132,19 @@
}
@Override
- public boolean setState(final int[] stateSet) {
+ public boolean setState(@NonNull int[] stateSet) {
boolean handled = mDrawable.setState(stateSet);
handled = updateTint(stateSet) || handled;
return handled;
}
+ @NonNull
@Override
public int[] getState() {
return mDrawable.getState();
}
+ @NonNull
@Override
public Drawable getCurrent() {
return mDrawable.getCurrent();
@@ -196,7 +186,7 @@
}
@Override
- public boolean getPadding(Rect padding) {
+ public boolean getPadding(@NonNull Rect padding) {
return mDrawable.getPadding(padding);
}
@@ -210,6 +200,7 @@
return null;
}
+ @NonNull
@Override
public Drawable mutate() {
if (!mMutated && super.mutate() == this) {
@@ -242,7 +233,7 @@
* {@inheritDoc}
*/
@Override
- public void invalidateDrawable(Drawable who) {
+ public void invalidateDrawable(@NonNull Drawable who) {
invalidateSelf();
}
@@ -250,7 +241,7 @@
* {@inheritDoc}
*/
@Override
- public void scheduleDrawable(Drawable who, Runnable what, long when) {
+ public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
scheduleSelf(what, when);
}
@@ -258,7 +249,7 @@
* {@inheritDoc}
*/
@Override
- public void unscheduleDrawable(Drawable who, Runnable what) {
+ public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
unscheduleSelf(what);
}
@@ -279,7 +270,7 @@
}
@Override
- public void setTintMode(PorterDuff.Mode tintMode) {
+ public void setTintMode(@NonNull PorterDuff.Mode tintMode) {
mState.mTintMode = tintMode;
updateTint(getState());
}
@@ -364,11 +355,13 @@
}
}
+ @NonNull
@Override
public Drawable newDrawable() {
return newDrawable(null);
}
+ @NonNull
@Override
public abstract Drawable newDrawable(@Nullable Resources res);
@@ -389,9 +382,10 @@
super(orig, res);
}
+ @NonNull
@Override
public Drawable newDrawable(@Nullable Resources res) {
- return new DrawableWrapperApi14(this, res);
+ return new WrappedDrawableApi14(this, res);
}
}
}
diff --git a/android/support/v4/graphics/drawable/DrawableWrapperApi19.java b/android/support/v4/graphics/drawable/WrappedDrawableApi19.java
similarity index 87%
rename from android/support/v4/graphics/drawable/DrawableWrapperApi19.java
rename to android/support/v4/graphics/drawable/WrappedDrawableApi19.java
index 7707591..7d6b8d8 100644
--- a/android/support/v4/graphics/drawable/DrawableWrapperApi19.java
+++ b/android/support/v4/graphics/drawable/WrappedDrawableApi19.java
@@ -23,13 +23,13 @@
import android.support.annotation.RequiresApi;
@RequiresApi(19)
-class DrawableWrapperApi19 extends DrawableWrapperApi14 {
+class WrappedDrawableApi19 extends WrappedDrawableApi14 {
- DrawableWrapperApi19(Drawable drawable) {
+ WrappedDrawableApi19(Drawable drawable) {
super(drawable);
}
- DrawableWrapperApi19(DrawableWrapperState state, Resources resources) {
+ WrappedDrawableApi19(DrawableWrapperState state, Resources resources) {
super(state, resources);
}
@@ -55,9 +55,10 @@
super(orig, res);
}
+ @NonNull
@Override
public Drawable newDrawable(@Nullable Resources res) {
- return new DrawableWrapperApi19(this, res);
+ return new WrappedDrawableApi19(this, res);
}
}
}
diff --git a/android/support/v4/graphics/drawable/DrawableWrapperApi21.java b/android/support/v4/graphics/drawable/WrappedDrawableApi21.java
similarity index 88%
rename from android/support/v4/graphics/drawable/DrawableWrapperApi21.java
rename to android/support/v4/graphics/drawable/WrappedDrawableApi21.java
index 5195cc9..b550742 100644
--- a/android/support/v4/graphics/drawable/DrawableWrapperApi21.java
+++ b/android/support/v4/graphics/drawable/WrappedDrawableApi21.java
@@ -16,8 +16,6 @@
package android.support.v4.graphics.drawable;
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Outline;
@@ -32,22 +30,21 @@
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
-import android.support.annotation.RestrictTo;
import android.util.Log;
import java.lang.reflect.Method;
@RequiresApi(21)
-class DrawableWrapperApi21 extends DrawableWrapperApi19 {
- private static final String TAG = "DrawableWrapperApi21";
+class WrappedDrawableApi21 extends WrappedDrawableApi19 {
+ private static final String TAG = "WrappedDrawableApi21";
private static Method sIsProjectedDrawableMethod;
- DrawableWrapperApi21(Drawable drawable) {
+ WrappedDrawableApi21(Drawable drawable) {
super(drawable);
findAndCacheIsProjectedDrawableMethod();
}
- DrawableWrapperApi21(DrawableWrapperState state, Resources resources) {
+ WrappedDrawableApi21(DrawableWrapperState state, Resources resources) {
super(state, resources);
findAndCacheIsProjectedDrawableMethod();
}
@@ -63,10 +60,11 @@
}
@Override
- public void getOutline(Outline outline) {
+ public void getOutline(@NonNull Outline outline) {
mDrawable.getOutline(outline);
}
+ @NonNull
@Override
public Rect getDirtyBounds() {
return mDrawable.getDirtyBounds();
@@ -100,7 +98,7 @@
}
@Override
- public boolean setState(int[] stateSet) {
+ public boolean setState(@NonNull int[] stateSet) {
if (super.setState(stateSet)) {
// Manually invalidate because the framework doesn't currently force an invalidation
// on a state change
@@ -123,9 +121,9 @@
}
/**
- * @hide
+ * This method is overriding hidden framework method in {@link Drawable}. It is used by the
+ * system and thus it should not be removed.
*/
- @RestrictTo(LIBRARY_GROUP)
public boolean isProjected() {
if (mDrawable != null && sIsProjectedDrawableMethod != null) {
try {
@@ -150,9 +148,10 @@
super(orig, res);
}
+ @NonNull
@Override
public Drawable newDrawable(@Nullable Resources res) {
- return new DrawableWrapperApi21(this, res);
+ return new WrappedDrawableApi21(this, res);
}
}
diff --git a/android/support/v4/util/ArraySet.java b/android/support/v4/util/ArraySet.java
index ab080fa..8444d2c 100644
--- a/android/support/v4/util/ArraySet.java
+++ b/android/support/v4/util/ArraySet.java
@@ -18,6 +18,8 @@
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.util.Log;
@@ -69,16 +71,15 @@
* The first entry in the array is a pointer to the next array in the
* list; the second entry is a pointer to the int[] hash code array for it.
*/
- static Object[] sBaseCache;
- static int sBaseCacheSize;
- static Object[] sTwiceBaseCache;
- static int sTwiceBaseCacheSize;
+ private static Object[] sBaseCache;
+ private static int sBaseCacheSize;
+ private static Object[] sTwiceBaseCache;
+ private static int sTwiceBaseCacheSize;
- final boolean mIdentityHashCode;
- int[] mHashes;
- Object[] mArray;
- int mSize;
- MapCollections<E, E> mCollections;
+ private int[] mHashes;
+ private Object[] mArray;
+ private int mSize;
+ private MapCollections<E, E> mCollections;
private int indexOf(Object key, int hash) {
final int N = mSize;
@@ -238,19 +239,13 @@
* will grow once items are added to it.
*/
public ArraySet() {
- this(0, false);
+ this(0);
}
/**
* Create a new ArraySet with a given initial capacity.
*/
public ArraySet(int capacity) {
- this(capacity, false);
- }
-
- /** {@hide} */
- public ArraySet(int capacity, boolean identityHashCode) {
- mIdentityHashCode = identityHashCode;
if (capacity == 0) {
mHashes = INT;
mArray = OBJECT;
@@ -263,15 +258,17 @@
/**
* Create a new ArraySet with the mappings from the given ArraySet.
*/
- public ArraySet(ArraySet<E> set) {
+ public ArraySet(@Nullable ArraySet<E> set) {
this();
if (set != null) {
addAll(set);
}
}
- /** {@hide} */
- public ArraySet(Collection<E> set) {
+ /**
+ * Create a new ArraySet with the mappings from the given {@link Collection}.
+ */
+ public ArraySet(@Nullable Collection<E> set) {
this();
if (set != null) {
addAll(set);
@@ -326,8 +323,7 @@
* @return Returns the index of the value if it exists, else a negative integer.
*/
public int indexOf(Object key) {
- return key == null ? indexOfNull()
- : indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
+ return key == null ? indexOfNull() : indexOf(key, key.hashCode());
}
/**
@@ -335,6 +331,7 @@
* @param index The desired index, must be between 0 and {@link #size()}-1.
* @return Returns the value stored at the given index.
*/
+ @Nullable
public E valueAt(int index) {
return (E) mArray[index];
}
@@ -357,14 +354,14 @@
* when the class of the object is inappropriate for this set.
*/
@Override
- public boolean add(E value) {
+ public boolean add(@Nullable E value) {
final int hash;
int index;
if (value == null) {
hash = 0;
index = indexOfNull();
} else {
- hash = mIdentityHashCode ? System.identityHashCode(value) : value.hashCode();
+ hash = value.hashCode();
index = indexOf(value, hash);
}
if (index >= 0) {
@@ -413,8 +410,7 @@
@RestrictTo(LIBRARY_GROUP)
public void append(E value) {
final int index = mSize;
- final int hash = value == null ? 0
- : (mIdentityHashCode ? System.identityHashCode(value) : value.hashCode());
+ final int hash = value == null ? 0 : value.hashCode();
if (index >= mHashes.length) {
throw new IllegalStateException("Array is full");
}
@@ -439,7 +435,7 @@
* Perform a {@link #add(Object)} of all values in <var>array</var>
* @param array The array whose contents are to be retrieved.
*/
- public void addAll(ArraySet<? extends E> array) {
+ public void addAll(@NonNull ArraySet<? extends E> array) {
final int N = array.mSize;
ensureCapacity(mSize + N);
if (mSize == 0) {
@@ -555,6 +551,7 @@
return mSize;
}
+ @NonNull
@Override
public Object[] toArray() {
Object[] result = new Object[mSize];
@@ -562,8 +559,9 @@
return result;
}
+ @NonNull
@Override
- public <T> T[] toArray(T[] array) {
+ public <T> T[] toArray(@NonNull T[] array) {
if (array.length < mSize) {
@SuppressWarnings("unchecked") T[] newArray =
(T[]) Array.newInstance(array.getClass().getComponentType(), mSize);
@@ -732,7 +730,7 @@
* in <var>collection</var>, else returns false.
*/
@Override
- public boolean containsAll(Collection<?> collection) {
+ public boolean containsAll(@NonNull Collection<?> collection) {
Iterator<?> it = collection.iterator();
while (it.hasNext()) {
if (!contains(it.next())) {
@@ -747,7 +745,7 @@
* @param collection The collection whose contents are to be retrieved.
*/
@Override
- public boolean addAll(Collection<? extends E> collection) {
+ public boolean addAll(@NonNull Collection<? extends E> collection) {
ensureCapacity(mSize + collection.size());
boolean added = false;
for (E value : collection) {
@@ -762,7 +760,7 @@
* @return Returns true if any values were removed from the array set, else false.
*/
@Override
- public boolean removeAll(Collection<?> collection) {
+ public boolean removeAll(@NonNull Collection<?> collection) {
boolean removed = false;
for (Object value : collection) {
removed |= remove(value);
@@ -777,7 +775,7 @@
* @return Returns true if any values were removed from the array set, else false.
*/
@Override
- public boolean retainAll(Collection<?> collection) {
+ public boolean retainAll(@NonNull Collection<?> collection) {
boolean removed = false;
for (int i = mSize - 1; i >= 0; i--) {
if (!collection.contains(mArray[i])) {
diff --git a/android/support/v4/util/LongSparseArray.java b/android/support/v4/util/LongSparseArray.java
index 25b6bb9..febb5d5 100644
--- a/android/support/v4/util/LongSparseArray.java
+++ b/android/support/v4/util/LongSparseArray.java
@@ -235,6 +235,14 @@
}
/**
+ * Return true if size() is 0.
+ * @return true if size() is 0.
+ */
+ public boolean isEmpty() {
+ return size() == 0;
+ }
+
+ /**
* Given an index in the range <code>0...size()-1</code>, returns
* the key from the <code>index</code>th key-value mapping that this
* LongSparseArray stores.
diff --git a/android/support/v4/util/ObjectsCompat.java b/android/support/v4/util/ObjectsCompat.java
index b6c740e..747cfb4 100644
--- a/android/support/v4/util/ObjectsCompat.java
+++ b/android/support/v4/util/ObjectsCompat.java
@@ -18,6 +18,7 @@
import android.os.Build;
import android.support.annotation.Nullable;
+import java.util.Arrays;
import java.util.Objects;
/**
@@ -51,4 +52,46 @@
return (a == b) || (a != null && a.equals(b));
}
}
+
+ /**
+ * Returns the hash code of a non-{@code null} argument and 0 for a {@code null} argument.
+ *
+ * @param o an object
+ * @return the hash code of a non-{@code null} argument and 0 for a {@code null} argument
+ * @see Object#hashCode
+ */
+ public static int hashCode(@Nullable Object o) {
+ return o != null ? o.hashCode() : 0;
+ }
+
+ /**
+ * Generates a hash code for a sequence of input values. The hash code is generated as if all
+ * the input values were placed into an array, and that array were hashed by calling
+ * {@link Arrays#hashCode(Object[])}.
+ *
+ * <p>This method is useful for implementing {@link Object#hashCode()} on objects containing
+ * multiple fields. For example, if an object that has three fields, {@code x}, {@code y}, and
+ * {@code z}, one could write:
+ *
+ * <blockquote><pre>
+ * @Override public int hashCode() {
+ * return ObjectsCompat.hash(x, y, z);
+ * }
+ * </pre></blockquote>
+ *
+ * <b>Warning: When a single object reference is supplied, the returned value does not equal the
+ * hash code of that object reference.</b> This value can be computed by calling
+ * {@link #hashCode(Object)}.
+ *
+ * @param values the values to be hashed
+ * @return a hash value of the sequence of input values
+ * @see Arrays#hashCode(Object[])
+ */
+ public static int hash(@Nullable Object... values) {
+ if (Build.VERSION.SDK_INT >= 19) {
+ return Objects.hash(values);
+ } else {
+ return Arrays.hashCode(values);
+ }
+ }
}
diff --git a/android/support/v4/util/SparseArrayCompat.java b/android/support/v4/util/SparseArrayCompat.java
index aedc4ad..5238cf0 100644
--- a/android/support/v4/util/SparseArrayCompat.java
+++ b/android/support/v4/util/SparseArrayCompat.java
@@ -228,6 +228,14 @@
}
/**
+ * Return true if size() is 0.
+ * @return true if size() is 0.
+ */
+ public boolean isEmpty() {
+ return size() == 0;
+ }
+
+ /**
* Given an index in the range <code>0...size()-1</code>, returns
* the key from the <code>index</code>th key-value mapping that this
* SparseArray stores.
diff --git a/android/support/v4/view/ViewCompat.java b/android/support/v4/view/ViewCompat.java
index 204a121..abdaa1a 100644
--- a/android/support/v4/view/ViewCompat.java
+++ b/android/support/v4/view/ViewCompat.java
@@ -60,6 +60,7 @@
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.WeakHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* Helper for accessing features in {@link View}.
@@ -443,6 +444,7 @@
private static Field sMinHeightField;
private static boolean sMinHeightFieldFetched;
private static WeakHashMap<View, String> sTransitionNameMap;
+ private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);
private Method mDispatchStartTemporaryDetach;
private Method mDispatchFinishTemporaryDetach;
private boolean mTempDetachBound;
@@ -1020,6 +1022,21 @@
public boolean isImportantForAutofill(@NonNull View v) {
return true;
}
+
+ /**
+ * {@link ViewCompat#generateViewId()}
+ */
+ public int generateViewId() {
+ for (;;) {
+ final int result = sNextGeneratedId.get();
+ // aapt-generated IDs have the high byte nonzero; clamp to the range under that.
+ int newValue = result + 1;
+ if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.
+ if (sNextGeneratedId.compareAndSet(result, newValue)) {
+ return result;
+ }
+ }
+ }
}
@RequiresApi(15)
@@ -1178,6 +1195,11 @@
public Display getDisplay(View view) {
return view.getDisplay();
}
+
+ @Override
+ public int generateViewId() {
+ return View.generateViewId();
+ }
}
@RequiresApi(18)
@@ -2413,6 +2435,30 @@
}
/**
+ * Finds the first descendant view with the given ID, the view itself if the ID matches
+ * {@link View#getId()}, or throws an IllegalArgumentException if the ID is invalid or there
+ * is no matching view in the hierarchy.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see View#findViewById(int)
+ */
+ @NonNull
+ public static <T extends View> T requireViewById(@NonNull View view, @IdRes int id) {
+ // TODO: use and link to View#requireViewById() directly, once available
+ T targetView = view.findViewById(id);
+ if (targetView == null) {
+ throw new IllegalArgumentException("ID does not reference a View inside this View");
+ }
+ return targetView;
+ }
+
+ /**
* Indicates whether this View is opaque. An opaque View guarantees that it will
* draw all the pixels overlapping its bounds using a fully opaque color.
*
@@ -3931,5 +3977,15 @@
return IMPL.hasExplicitFocusable(view);
}
+ /**
+ * Generate a value suitable for use in {@link View#setId(int)}.
+ * This value will not collide with ID values generated at build time by aapt for R.id.
+ *
+ * @return a generated ID value
+ */
+ public static int generateViewId() {
+ return IMPL.generateViewId();
+ }
+
protected ViewCompat() {}
}
diff --git a/android/support/v4/view/ViewConfigurationCompat.java b/android/support/v4/view/ViewConfigurationCompat.java
index 60d37a9..a12387b 100644
--- a/android/support/v4/view/ViewConfigurationCompat.java
+++ b/android/support/v4/view/ViewConfigurationCompat.java
@@ -117,5 +117,17 @@
return 0;
}
+ /**
+ * @param config Used to get the hover slop directly from the {@link ViewConfiguration}.
+ *
+ * @return The hover slop value.
+ */
+ public static int getScaledHoverSlop(ViewConfiguration config) {
+ if (android.os.Build.VERSION.SDK_INT >= 28) {
+ return config.getScaledHoverSlop();
+ }
+ return config.getScaledTouchSlop() / 2;
+ }
+
private ViewConfigurationCompat() {}
}
diff --git a/android/support/v4/view/WindowCompat.java b/android/support/v4/view/WindowCompat.java
index cdf4789..dd0a736 100644
--- a/android/support/v4/view/WindowCompat.java
+++ b/android/support/v4/view/WindowCompat.java
@@ -16,6 +16,8 @@
package android.support.v4.view;
+import android.support.annotation.IdRes;
+import android.support.annotation.NonNull;
import android.view.View;
import android.view.Window;
@@ -59,4 +61,29 @@
public static final int FEATURE_ACTION_MODE_OVERLAY = 10;
private WindowCompat() {}
+
+ /**
+ * Finds a view that was identified by the {@code android:id} XML attribute
+ * that was processed in {@link android.app.Activity#onCreate}, or throws an
+ * IllegalArgumentException if the ID is invalid, or there is no matching view in the hierarchy.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see ViewCompat#requireViewById(View, int)
+ * @see Window#findViewById(int)
+ */
+ @NonNull
+ public static <T extends View> T requireViewById(@NonNull Window window, @IdRes int id) {
+ // TODO: use and link to Window#requireViewById() directly, once available
+ T view = window.findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException("ID does not reference a View inside this Window");
+ }
+ return view;
+ }
}
diff --git a/android/support/v4/widget/ContentLoadingProgressBar.java b/android/support/v4/widget/ContentLoadingProgressBar.java
index 356c7b9..631bec5 100644
--- a/android/support/v4/widget/ContentLoadingProgressBar.java
+++ b/android/support/v4/widget/ContentLoadingProgressBar.java
@@ -93,9 +93,10 @@
* hidden until it has been shown for at least a minimum show time. If the
* progress view was not yet visible, cancels showing the progress view.
*/
- public void hide() {
+ public synchronized void hide() {
mDismissed = true;
removeCallbacks(mDelayedShow);
+ mPostedShow = false;
long diff = System.currentTimeMillis() - mStartTime;
if (diff >= MIN_SHOW_TIME || mStartTime == -1) {
// The progress spinner has been shown long enough
@@ -117,11 +118,12 @@
* Show the progress view after waiting for a minimum delay. If
* during that time, hide() is called, the view is never made visible.
*/
- public void show() {
+ public synchronized void show() {
// Reset the start time.
mStartTime = -1;
mDismissed = false;
removeCallbacks(mDelayedHide);
+ mPostedHide = false;
if (!mPostedShow) {
postDelayed(mDelayedShow, MIN_DELAY);
mPostedShow = true;
diff --git a/android/support/v4/widget/CursorAdapter.java b/android/support/v4/widget/CursorAdapter.java
index e68229e..3ea6fc8 100644
--- a/android/support/v4/widget/CursorAdapter.java
+++ b/android/support/v4/widget/CursorAdapter.java
@@ -43,55 +43,55 @@
CursorFilter.CursorFilterClient {
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected boolean mDataValid;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected boolean mAutoRequery;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected Cursor mCursor;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected Context mContext;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected int mRowIDColumn;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected ChangeObserver mChangeObserver;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected DataSetObserver mDataSetObserver;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected CursorFilter mCursorFilter;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected FilterQueryProvider mFilterQueryProvider;
diff --git a/android/support/v4/widget/SimpleCursorAdapter.java b/android/support/v4/widget/SimpleCursorAdapter.java
index 291f9e1..ba3ee50 100644
--- a/android/support/v4/widget/SimpleCursorAdapter.java
+++ b/android/support/v4/widget/SimpleCursorAdapter.java
@@ -37,14 +37,14 @@
/**
* A list of columns containing the data to bind to the UI.
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected int[] mFrom;
/**
* A list of View ids representing the views to which the data must be bound.
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected int[] mTo;
diff --git a/android/support/v4/widget/TextViewCompat.java b/android/support/v4/widget/TextViewCompat.java
index dc87a38..8789815 100644
--- a/android/support/v4/widget/TextViewCompat.java
+++ b/android/support/v4/widget/TextViewCompat.java
@@ -18,6 +18,11 @@
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.DrawableRes;
@@ -28,14 +33,22 @@
import android.support.annotation.RestrictTo;
import android.support.annotation.StyleRes;
import android.support.v4.os.BuildCompat;
+import android.text.Editable;
import android.util.Log;
import android.util.TypedValue;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
/**
* Helper for accessing features in {@link TextView}.
@@ -219,6 +232,11 @@
}
return new int[0];
}
+
+ public void setCustomSelectionActionModeCallback(TextView textView,
+ ActionMode.Callback callback) {
+ textView.setCustomSelectionActionModeCallback(callback);
+ }
}
@RequiresApi(16)
@@ -314,8 +332,160 @@
}
}
+ @RequiresApi(26)
+ static class TextViewCompatApi26Impl extends TextViewCompatApi23Impl {
+ @Override
+ public void setCustomSelectionActionModeCallback(final TextView textView,
+ final ActionMode.Callback callback) {
+ if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O
+ && Build.VERSION.SDK_INT != Build.VERSION_CODES.O_MR1) {
+ super.setCustomSelectionActionModeCallback(textView, callback);
+ return;
+ }
+
+
+ // A bug in O and O_MR1 causes a number of options for handling the ACTION_PROCESS_TEXT
+ // intent after selection to not be displayed in the menu, although they should be.
+ // Here we fix this, by removing the menu items created by the framework code, and
+ // adding them (and the missing ones) back correctly.
+ textView.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
+ // This constant should be correlated with its definition in the
+ // android.widget.Editor class.
+ private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
+
+ // References to the MenuBuilder class and its removeItemAt(int) method.
+ // Since in most cases the menu instance processed by this callback is going
+ // to be a MenuBuilder, we keep these references to avoid querying for them
+ // frequently by reflection in recomputeProcessTextMenuItems.
+ private Class mMenuBuilderClass;
+ private Method mMenuBuilderRemoveItemAtMethod;
+ private boolean mCanUseMenuBuilderReferences;
+ private boolean mInitializedMenuBuilderReferences = false;
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ return callback.onCreateActionMode(mode, menu);
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ recomputeProcessTextMenuItems(menu);
+ return callback.onPrepareActionMode(mode, menu);
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return callback.onActionItemClicked(mode, item);
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ callback.onDestroyActionMode(mode);
+ }
+
+ private void recomputeProcessTextMenuItems(final Menu menu) {
+ final Context context = textView.getContext();
+ final PackageManager packageManager = context.getPackageManager();
+
+ if (!mInitializedMenuBuilderReferences) {
+ mInitializedMenuBuilderReferences = true;
+ try {
+ mMenuBuilderClass =
+ Class.forName("com.android.internal.view.menu.MenuBuilder");
+ mMenuBuilderRemoveItemAtMethod = mMenuBuilderClass
+ .getDeclaredMethod("removeItemAt", Integer.TYPE);
+ mCanUseMenuBuilderReferences = true;
+ } catch (ClassNotFoundException | NoSuchMethodException e) {
+ mMenuBuilderClass = null;
+ mMenuBuilderRemoveItemAtMethod = null;
+ mCanUseMenuBuilderReferences = false;
+ }
+ }
+ // Remove the menu items created for ACTION_PROCESS_TEXT handlers.
+ try {
+ final Method removeItemAtMethod =
+ (mCanUseMenuBuilderReferences && mMenuBuilderClass.isInstance(menu))
+ ? mMenuBuilderRemoveItemAtMethod
+ : menu.getClass()
+ .getDeclaredMethod("removeItemAt", Integer.TYPE);
+ for (int i = menu.size() - 1; i >= 0; --i) {
+ final MenuItem item = menu.getItem(i);
+ if (item.getIntent() != null && Intent.ACTION_PROCESS_TEXT
+ .equals(item.getIntent().getAction())) {
+ removeItemAtMethod.invoke(menu, i);
+ }
+ }
+ } catch (NoSuchMethodException | IllegalAccessException
+ | InvocationTargetException e) {
+ // There is a menu custom implementation used which is not providing
+ // a removeItemAt(int) menu. There is nothing we can do in this case.
+ return;
+ }
+
+ // Populate the menu again with the ACTION_PROCESS_TEXT handlers.
+ final List<ResolveInfo> supportedActivities =
+ getSupportedActivities(context, packageManager);
+ for (int i = 0; i < supportedActivities.size(); ++i) {
+ final ResolveInfo info = supportedActivities.get(i);
+ menu.add(Menu.NONE, Menu.NONE,
+ MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
+ info.loadLabel(packageManager))
+ .setIntent(createProcessTextIntentForResolveInfo(info, textView))
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ }
+ }
+
+ private List<ResolveInfo> getSupportedActivities(final Context context,
+ final PackageManager packageManager) {
+ final List<ResolveInfo> supportedActivities = new ArrayList<>();
+ boolean canStartActivityForResult = context instanceof Activity;
+ if (!canStartActivityForResult) {
+ return supportedActivities;
+ }
+ final List<ResolveInfo> unfiltered =
+ packageManager.queryIntentActivities(createProcessTextIntent(), 0);
+ for (ResolveInfo info : unfiltered) {
+ if (isSupportedActivity(info, context)) {
+ supportedActivities.add(info);
+ }
+ }
+ return supportedActivities;
+ }
+
+ private boolean isSupportedActivity(final ResolveInfo info, final Context context) {
+ if (context.getPackageName().equals(info.activityInfo.packageName)) {
+ return true;
+ }
+ if (!info.activityInfo.exported) {
+ return false;
+ }
+ return info.activityInfo.permission == null
+ || context.checkSelfPermission(info.activityInfo.permission)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private Intent createProcessTextIntentForResolveInfo(final ResolveInfo info,
+ final TextView textView) {
+ return createProcessTextIntent()
+ .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !isEditable(textView))
+ .setClassName(info.activityInfo.packageName, info.activityInfo.name);
+ }
+
+ private boolean isEditable(final TextView textView) {
+ return textView instanceof Editable
+ && textView.onCheckIsTextEditor()
+ && textView.isEnabled();
+ }
+
+ private Intent createProcessTextIntent() {
+ return new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain");
+ }
+ });
+ }
+ }
+
@RequiresApi(27)
- static class TextViewCompatApi27Impl extends TextViewCompatApi23Impl {
+ static class TextViewCompatApi27Impl extends TextViewCompatApi26Impl {
@Override
public void setAutoSizeTextTypeWithDefaults(TextView textView, int autoSizeTextType) {
textView.setAutoSizeTextTypeWithDefaults(autoSizeTextType);
@@ -369,6 +539,8 @@
static {
if (BuildCompat.isAtLeastOMR1()) {
IMPL = new TextViewCompatApi27Impl();
+ } else if (Build.VERSION.SDK_INT >= 26) {
+ IMPL = new TextViewCompatApi26Impl();
} else if (Build.VERSION.SDK_INT >= 23) {
IMPL = new TextViewCompatApi23Impl();
} else if (Build.VERSION.SDK_INT >= 18) {
@@ -600,4 +772,31 @@
public static int[] getAutoSizeTextAvailableSizes(@NonNull TextView textView) {
return IMPL.getAutoSizeTextAvailableSizes(textView);
}
+
+ /**
+ * Sets a selection action mode callback on a TextView.
+ *
+ * Also this method can be used to fix a bug in framework SDK 26. On these affected devices,
+ * the bug causes the menu containing the options for handling ACTION_PROCESS_TEXT after text
+ * selection to miss a number of items. This method can be used to fix this wrong behaviour for
+ * a text view, by passing any custom callback implementation. If no custom callback is desired,
+ * a no-op implementation should be provided.
+ *
+ * Note that, by default, the bug will only be fixed when the default floating toolbar menu
+ * implementation is used. If a custom implementation of {@link Menu} is provided, this should
+ * provide the method Menu#removeItemAt(int) which removes a menu item by its position,
+ * as given by Menu#getItem(int). Also, the following post condition should hold: a call
+ * to removeItemAt(i), should not modify the results of getItem(j) for any j < i. Intuitively,
+ * removing an element from the menu should behave as removing an element from a list.
+ * Note that this method does not exist in the {@link Menu} interface. However, it is required,
+ * and going to be called by reflection, in order to display the correct process text items in
+ * the menu.
+ *
+ * @param textView The TextView to set the action selection mode callback on.
+ * @param callback The action selection mode callback to set on textView.
+ */
+ public static void setCustomSelectionActionModeCallback(@NonNull TextView textView,
+ @NonNull ActionMode.Callback callback) {
+ IMPL.setCustomSelectionActionModeCallback(textView, callback);
+ }
}
diff --git a/android/support/v7/preference/Preference.java b/android/support/v7/preference/Preference.java
index fa8461d..88262cd 100644
--- a/android/support/v7/preference/Preference.java
+++ b/android/support/v7/preference/Preference.java
@@ -16,6 +16,7 @@
package android.support.v7.preference;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.content.Context;
@@ -1297,6 +1298,7 @@
* preference was removed, modified, and re-added to a {@link PreferenceGroup}
* @hide
*/
+ @RestrictTo(LIBRARY)
public final boolean wasDetached() {
return mWasDetached;
}
@@ -1305,6 +1307,7 @@
* Clears the {@link #wasDetached()} status
* @hide
*/
+ @RestrictTo(LIBRARY)
public final void clearWasDetached() {
mWasDetached = false;
}
diff --git a/android/support/v7/recyclerview/extensions/ListAdapter.java b/android/support/v7/recyclerview/extensions/ListAdapter.java
index 8b28072..721e0da 100644
--- a/android/support/v7/recyclerview/extensions/ListAdapter.java
+++ b/android/support/v7/recyclerview/extensions/ListAdapter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 2017 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.
@@ -17,6 +17,8 @@
package android.support.v7.recyclerview.extensions;
import android.support.annotation.NonNull;
+import android.support.v7.util.AdapterListUpdateCallback;
+import android.support.v7.util.DiffUtil;
import android.support.v7.widget.RecyclerView;
import java.util.List;
@@ -66,7 +68,8 @@
* public void onBindViewHolder(UserViewHolder holder, int position) {
* holder.bindTo(getItem(position));
* }
- * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<User>() {
+ * public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
+ * new DiffUtil.ItemCallback<User>() {
* {@literal @}Override
* public boolean areItemsTheSame(
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
@@ -95,14 +98,14 @@
private final ListAdapterHelper<T> mHelper;
@SuppressWarnings("unused")
- protected ListAdapter(@NonNull DiffCallback<T> diffCallback) {
- mHelper = new ListAdapterHelper<>(new ListAdapterHelper.AdapterCallback(this),
- new ListAdapterConfig.Builder<T>().setDiffCallback(diffCallback).build());
+ protected ListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
+ mHelper = new ListAdapterHelper<>(new AdapterListUpdateCallback(this),
+ new ListAdapterConfig.Builder<>(diffCallback).build());
}
@SuppressWarnings("unused")
protected ListAdapter(@NonNull ListAdapterConfig<T> config) {
- mHelper = new ListAdapterHelper<>(new ListAdapterHelper.AdapterCallback(this), config);
+ mHelper = new ListAdapterHelper<>(new AdapterListUpdateCallback(this), config);
}
/**
diff --git a/android/support/v7/recyclerview/extensions/ListAdapterConfig.java b/android/support/v7/recyclerview/extensions/ListAdapterConfig.java
index 25697a1..53fe4bb 100644
--- a/android/support/v7/recyclerview/extensions/ListAdapterConfig.java
+++ b/android/support/v7/recyclerview/extensions/ListAdapterConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 2017 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.
@@ -16,79 +16,91 @@
package android.support.v7.recyclerview.extensions;
-import android.arch.core.executor.ArchTaskExecutor;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+import android.support.v7.util.DiffUtil;
import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
/**
* Configuration object for {@link ListAdapter}, {@link ListAdapterHelper}, and similar
* background-thread list diffing adapter logic.
* <p>
- * At minimum, defines item diffing behavior with a {@link DiffCallback}, used to compute item
- * differences to pass to a RecyclerView adapter.
+ * At minimum, defines item diffing behavior with a {@link DiffUtil.ItemCallback}, used to compute
+ * item differences to pass to a RecyclerView adapter.
*
* @param <T> Type of items in the lists, and being compared.
*/
public final class ListAdapterConfig<T> {
+ @NonNull
private final Executor mMainThreadExecutor;
+ @NonNull
private final Executor mBackgroundThreadExecutor;
- private final DiffCallback<T> mDiffCallback;
+ @NonNull
+ private final DiffUtil.ItemCallback<T> mDiffCallback;
- private ListAdapterConfig(Executor mainThreadExecutor, Executor backgroundThreadExecutor,
- DiffCallback<T> diffCallback) {
+ private ListAdapterConfig(
+ @NonNull Executor mainThreadExecutor,
+ @NonNull Executor backgroundThreadExecutor,
+ @NonNull DiffUtil.ItemCallback<T> diffCallback) {
mMainThreadExecutor = mainThreadExecutor;
mBackgroundThreadExecutor = backgroundThreadExecutor;
mDiffCallback = diffCallback;
}
+ /** @hide */
+ @SuppressWarnings("WeakerAccess")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
public Executor getMainThreadExecutor() {
return mMainThreadExecutor;
}
+ /** @hide */
+ @SuppressWarnings("WeakerAccess")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
public Executor getBackgroundThreadExecutor() {
return mBackgroundThreadExecutor;
}
- public DiffCallback<T> getDiffCallback() {
+ @SuppressWarnings("WeakerAccess")
+ @NonNull
+ public DiffUtil.ItemCallback<T> getDiffCallback() {
return mDiffCallback;
}
/**
* Builder class for {@link ListAdapterConfig}.
- * <p>
- * You must at minimum specify a DiffCallback with {@link #setDiffCallback(DiffCallback)}
*
* @param <T>
*/
public static class Builder<T> {
private Executor mMainThreadExecutor;
private Executor mBackgroundThreadExecutor;
- private DiffCallback<T> mDiffCallback;
+ private final DiffUtil.ItemCallback<T> mDiffCallback;
- /**
- * The {@link DiffCallback} to be used while diffing an old list with the updated one.
- * Must be provided.
- *
- * @param diffCallback The {@link DiffCallback} instance to compare items in the list.
- * @return this
- */
- @SuppressWarnings("WeakerAccess")
- public ListAdapterConfig.Builder<T> setDiffCallback(DiffCallback<T> diffCallback) {
+ public Builder(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
mDiffCallback = diffCallback;
- return this;
}
/**
* If provided, defines the main thread executor used to dispatch adapter update
* notifications on the main thread.
* <p>
- * If not provided, it will default to the UI thread.
+ * If not provided, it will default to the main thread.
*
* @param executor The executor which can run tasks in the UI thread.
* @return this
+ *
+ * @hide
*/
- @SuppressWarnings("unused")
- public ListAdapterConfig.Builder<T> setMainThreadExecutor(Executor executor) {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public Builder<T> setMainThreadExecutor(Executor executor) {
mMainThreadExecutor = executor;
return this;
}
@@ -97,36 +109,55 @@
* If provided, defines the background executor used to calculate the diff between an old
* and a new list.
* <p>
- * If not provided, defaults to the IO thread pool from Architecture Components.
+ * If not provided, defaults to two thread pool executor, shared by all ListAdapterConfigs.
*
* @param executor The background executor to run list diffing.
* @return this
*/
- @SuppressWarnings("unused")
- public ListAdapterConfig.Builder<T> setBackgroundThreadExecutor(Executor executor) {
+ @SuppressWarnings({"unused", "WeakerAccess"})
+ @NonNull
+ public Builder<T> setBackgroundThreadExecutor(Executor executor) {
mBackgroundThreadExecutor = executor;
return this;
}
+ private static class MainThreadExecutor implements Executor {
+ final Handler mHandler = new Handler(Looper.getMainLooper());
+ @Override
+ public void execute(@NonNull Runnable command) {
+ mHandler.post(command);
+ }
+ }
+
/**
* Creates a {@link ListAdapterHelper} with the given parameters.
*
* @return A new ListAdapterConfig.
*/
+ @NonNull
public ListAdapterConfig<T> build() {
- if (mDiffCallback == null) {
- throw new IllegalArgumentException("Must provide a diffCallback");
+ if (mMainThreadExecutor == null) {
+ mMainThreadExecutor = sMainThreadExecutor;
}
if (mBackgroundThreadExecutor == null) {
- mBackgroundThreadExecutor = ArchTaskExecutor.getIOThreadExecutor();
- }
- if (mMainThreadExecutor == null) {
- mMainThreadExecutor = ArchTaskExecutor.getMainThreadExecutor();
+ synchronized (sExecutorLock) {
+ if (sDiffExecutor == null) {
+ sDiffExecutor = Executors.newFixedThreadPool(2);
+ }
+ }
+ mBackgroundThreadExecutor = sDiffExecutor;
}
return new ListAdapterConfig<>(
mMainThreadExecutor,
mBackgroundThreadExecutor,
mDiffCallback);
}
+
+ // TODO: remove the below once supportlib has its own appropriate executors
+ private static final Object sExecutorLock = new Object();
+ private static Executor sDiffExecutor = null;
+
+ // TODO: use MainThreadExecutor from supportlib once one exists
+ private static final Executor sMainThreadExecutor = new MainThreadExecutor();
}
}
diff --git a/android/support/v7/recyclerview/extensions/ListAdapterHelper.java b/android/support/v7/recyclerview/extensions/ListAdapterHelper.java
index d0c7bb3..bb231b1 100644
--- a/android/support/v7/recyclerview/extensions/ListAdapterHelper.java
+++ b/android/support/v7/recyclerview/extensions/ListAdapterHelper.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 2017 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.
@@ -16,8 +16,8 @@
package android.support.v7.recyclerview.extensions;
-import android.arch.lifecycle.LiveData;
-import android.support.annotation.RestrictTo;
+import android.support.annotation.NonNull;
+import android.support.v7.util.AdapterListUpdateCallback;
import android.support.v7.util.DiffUtil;
import android.support.v7.util.ListUpdateCallback;
import android.support.v7.widget.RecyclerView;
@@ -25,17 +25,18 @@
import java.util.List;
/**
- * Helper object for displaying a List in {@link RecyclerView.Adapter RecyclerView.Adapter}, which
- * signals the adapter of changes when the List is changed by computing changes with DiffUtil in the
+ * Helper object for displaying a List in
+ * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, which signals the
+ * adapter of changes when the List is changed by computing changes with DiffUtil in the
* background.
* <p>
* For simplicity, the {@link ListAdapter} wrapper class can often be used instead of the
* helper directly. This helper class is exposed for complex cases, and where overriding an adapter
* base class to support List diffing isn't convenient.
* <p>
- * The ListAdapterHelper can take a {@link LiveData} of List and present the data simply for an
- * adapter. It computes differences in List contents via DiffUtil on a background thread as new
- * Lists are received.
+ * The ListAdapterHelper can consume the values from a LiveData of <code>List</code> and present the
+ * data simply for an adapter. It computes differences in List contents via {@link DiffUtil} on a
+ * background thread as new <code>List</code>s are received.
* <p>
* It provides a simple list-like API with {@link #getItem(int)} and {@link #getItemCount()} for an
* adapter to acquire and present data objects.
@@ -68,10 +69,8 @@
* }
*
* class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
- * private final ListAdapterHelper<User> mHelper;
- * public UserAdapter(ListAdapterHelper.Builder<User> builder) {
- * mHelper = new ListAdapterHelper(this, User.DIFF_CALLBACK);
- * }
+ * private final ListAdapterHelper<User> mHelper =
+ * new ListAdapterHelper(this, DIFF_CALLBACK);
* {@literal @}Override
* public int getItemCount() {
* return mHelper.getItemCount();
@@ -84,7 +83,8 @@
* User user = mHelper.getItem(position);
* holder.bindTo(user);
* }
- * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<User>() {
+ * public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK
+ * = new DiffUtil.ItemCallback<User>() {
* {@literal @}Override
* public boolean areItemsTheSame(
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
@@ -107,47 +107,37 @@
private final ListUpdateCallback mUpdateCallback;
private final ListAdapterConfig<T> mConfig;
- @SuppressWarnings("WeakerAccess")
- public ListAdapterHelper(ListUpdateCallback listUpdateCallback,
- ListAdapterConfig<T> config) {
- mUpdateCallback = listUpdateCallback;
- mConfig = config;
+ /**
+ * Convenience for
+ * {@code PagedListAdapterHelper(new AdapterListUpdateCallback(adapter),
+ * new ListAdapterConfig.Builder().setDiffCallback(diffCallback).build());}
+ *
+ * @param adapter Adapter to dispatch position updates to.
+ * @param diffCallback ItemCallback that compares items to dispatch appropriate animations when
+ *
+ * @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter)
+ */
+ public ListAdapterHelper(@NonNull RecyclerView.Adapter adapter,
+ @NonNull DiffUtil.ItemCallback<T> diffCallback) {
+ mUpdateCallback = new AdapterListUpdateCallback(adapter);
+ mConfig = new ListAdapterConfig.Builder<>(diffCallback).build();
}
/**
- * Default ListUpdateCallback that dispatches directly to an adapter. Can be replaced by a
- * custom ListUpdateCallback if e.g. your adapter has a header in it, and so has an offset
- * between list positions and adapter positions.
+ * Create a ListAdapterHelper with the provided config, and ListUpdateCallback to dispatch
+ * updates to.
*
- * @hide
+ * @param listUpdateCallback Callback to dispatch updates to.
+ * @param config Config to define background work Executor, and DiffUtil.ItemCallback for
+ * computing List diffs.
+ *
+ * @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter)
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static class AdapterCallback implements ListUpdateCallback {
- private final RecyclerView.Adapter mAdapter;
-
- public AdapterCallback(RecyclerView.Adapter adapter) {
- mAdapter = adapter;
- }
-
- @Override
- public void onInserted(int position, int count) {
- mAdapter.notifyItemRangeInserted(position, count);
- }
-
- @Override
- public void onRemoved(int position, int count) {
- mAdapter.notifyItemRangeRemoved(position, count);
- }
-
- @Override
- public void onMoved(int fromPosition, int toPosition) {
- mAdapter.notifyItemMoved(fromPosition, toPosition);
- }
-
- @Override
- public void onChanged(int position, int count, Object payload) {
- mAdapter.notifyItemRangeChanged(position, count, payload);
- }
+ @SuppressWarnings("WeakerAccess")
+ public ListAdapterHelper(@NonNull ListUpdateCallback listUpdateCallback,
+ @NonNull ListAdapterConfig<T> config) {
+ mUpdateCallback = listUpdateCallback;
+ mConfig = config;
}
private List<T> mList;
@@ -173,7 +163,8 @@
/**
* Get the number of items currently presented by this AdapterHelper. This value can be directly
- * returned to {@link RecyclerView.Adapter#getItemCount()}.
+ * returned to {@link android.support.v7.widget.RecyclerView.Adapter#getItemCount()
+ * RecyclerView.Adapter.getItemCount()}.
*
* @return Number of items being presented.
*/
diff --git a/android/support/v7/util/AdapterListUpdateCallback.java b/android/support/v7/util/AdapterListUpdateCallback.java
new file mode 100644
index 0000000..f86ba7d
--- /dev/null
+++ b/android/support/v7/util/AdapterListUpdateCallback.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2018 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.support.v7.util;
+
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+
+/**
+ * ListUpdateCallback that dispatches update events to the given adapter.
+ *
+ * @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter)
+ */
+public final class AdapterListUpdateCallback implements ListUpdateCallback {
+ @NonNull
+ private final RecyclerView.Adapter mAdapter;
+
+ /**
+ * Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter.
+ *
+ * @param adapter The Adapter to send updates to.
+ */
+ public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
+ mAdapter = adapter;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onInserted(int position, int count) {
+ mAdapter.notifyItemRangeInserted(position, count);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onRemoved(int position, int count) {
+ mAdapter.notifyItemRangeRemoved(position, count);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onMoved(int fromPosition, int toPosition) {
+ mAdapter.notifyItemMoved(fromPosition, toPosition);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onChanged(int position, int count, Object payload) {
+ mAdapter.notifyItemRangeChanged(position, count, payload);
+ }
+}
diff --git a/android/support/v7/util/DiffUtil.java b/android/support/v7/util/DiffUtil.java
index ebc33f3..a55a21d 100644
--- a/android/support/v7/util/DiffUtil.java
+++ b/android/support/v7/util/DiffUtil.java
@@ -16,7 +16,6 @@
package android.support.v7.util;
-import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView;
@@ -369,7 +368,7 @@
*
* @see Callback#areItemsTheSame(int, int)
*/
- public abstract boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem);
+ public abstract boolean areItemsTheSame(T oldItem, T newItem);
/**
* Called to check whether two items have the same data.
@@ -392,7 +391,7 @@
*
* @see Callback#areContentsTheSame(int, int)
*/
- public abstract boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem);
+ public abstract boolean areContentsTheSame(T oldItem, T newItem);
/**
* When {@link #areItemsTheSame(T, T)} returns {@code true} for two items and
@@ -409,7 +408,7 @@
* @see Callback#getChangePayload(int, int)
*/
@SuppressWarnings({"WeakerAccess", "unused"})
- public Object getChangePayload(@NonNull T oldItem, @NonNull T newItem) {
+ public Object getChangePayload(T oldItem, T newItem) {
return null;
}
}
@@ -721,35 +720,16 @@
*
* @param adapter A RecyclerView adapter which was displaying the old list and will start
* displaying the new list.
+ * @see AdapterListUpdateCallback
*/
public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
- dispatchUpdatesTo(new ListUpdateCallback() {
- @Override
- public void onInserted(int position, int count) {
- adapter.notifyItemRangeInserted(position, count);
- }
-
- @Override
- public void onRemoved(int position, int count) {
- adapter.notifyItemRangeRemoved(position, count);
- }
-
- @Override
- public void onMoved(int fromPosition, int toPosition) {
- adapter.notifyItemMoved(fromPosition, toPosition);
- }
-
- @Override
- public void onChanged(int position, int count, Object payload) {
- adapter.notifyItemRangeChanged(position, count, payload);
- }
- });
+ dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
}
/**
* Dispatches update operations to the given Callback.
* <p>
- * These updates are atomic such that the first update call effects every update call that
+ * These updates are atomic such that the first update call affects every update call that
* comes after it (the same as RecyclerView).
*
* @param updateCallback The callback to receive the update operations.
diff --git a/android/support/v7/widget/AppCompatEditText.java b/android/support/v7/widget/AppCompatEditText.java
index 6831fcb..fdda68e 100644
--- a/android/support/v7/widget/AppCompatEditText.java
+++ b/android/support/v7/widget/AppCompatEditText.java
@@ -25,8 +25,10 @@
import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
+import android.support.v4.os.BuildCompat;
import android.support.v4.view.TintableBackgroundView;
import android.support.v7.appcompat.R;
+import android.text.Editable;
import android.util.AttributeSet;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
@@ -71,6 +73,20 @@
mTextHelper.applyCompoundDrawablesTints();
}
+ /**
+ * Return the text that the view is displaying. If an editable text has not been set yet, this
+ * will return null.
+ */
+ @Override
+ @Nullable public Editable getText() {
+ if (BuildCompat.isAtLeastP()) {
+ return super.getText();
+ }
+ // A bug pre-P makes getText() crash if called before the first setText due to a cast, so
+ // retrieve the editable text.
+ return super.getEditableText();
+ }
+
@Override
public void setBackgroundResource(@DrawableRes int resId) {
super.setBackgroundResource(resId);
diff --git a/android/support/v7/widget/AppCompatProgressBarHelper.java b/android/support/v7/widget/AppCompatProgressBarHelper.java
index 443281e..a95873c 100644
--- a/android/support/v7/widget/AppCompatProgressBarHelper.java
+++ b/android/support/v7/widget/AppCompatProgressBarHelper.java
@@ -27,7 +27,7 @@
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.graphics.drawable.shapes.Shape;
-import android.support.v4.graphics.drawable.DrawableWrapper;
+import android.support.v4.graphics.drawable.WrappedDrawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.widget.ProgressBar;
@@ -69,11 +69,11 @@
* traverse layer and state list drawables.
*/
private Drawable tileify(Drawable drawable, boolean clip) {
- if (drawable instanceof DrawableWrapper) {
- Drawable inner = ((DrawableWrapper) drawable).getWrappedDrawable();
+ if (drawable instanceof WrappedDrawable) {
+ Drawable inner = ((WrappedDrawable) drawable).getWrappedDrawable();
if (inner != null) {
inner = tileify(inner, clip);
- ((DrawableWrapper) drawable).setWrappedDrawable(inner);
+ ((WrappedDrawable) drawable).setWrappedDrawable(inner);
}
} else if (drawable instanceof LayerDrawable) {
LayerDrawable background = (LayerDrawable) drawable;
diff --git a/android/support/v7/widget/ContentFrameLayout.java b/android/support/v7/widget/ContentFrameLayout.java
index 1100280..f777901 100644
--- a/android/support/v7/widget/ContentFrameLayout.java
+++ b/android/support/v7/widget/ContentFrameLayout.java
@@ -16,6 +16,7 @@
package android.support.v7.widget;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static android.view.View.MeasureSpec.AT_MOST;
import static android.view.View.MeasureSpec.EXACTLY;
@@ -33,6 +34,7 @@
/**
* @hide
*/
+@RestrictTo(LIBRARY)
public class ContentFrameLayout extends FrameLayout {
public interface OnAttachListener {
diff --git a/android/support/v7/widget/DrawableUtils.java b/android/support/v7/widget/DrawableUtils.java
index c7820b6..9216726 100644
--- a/android/support/v7/widget/DrawableUtils.java
+++ b/android/support/v7/widget/DrawableUtils.java
@@ -30,6 +30,7 @@
import android.support.annotation.NonNull;
import android.support.annotation.RestrictTo;
import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v4.graphics.drawable.WrappedDrawable;
import android.util.Log;
import java.lang.reflect.Field;
@@ -146,9 +147,9 @@
}
}
}
- } else if (drawable instanceof android.support.v4.graphics.drawable.DrawableWrapper) {
+ } else if (drawable instanceof WrappedDrawable) {
return canSafelyMutateDrawable(
- ((android.support.v4.graphics.drawable.DrawableWrapper) drawable)
+ ((WrappedDrawable) drawable)
.getWrappedDrawable());
} else if (drawable instanceof android.support.v7.graphics.drawable.DrawableWrapper) {
return canSafelyMutateDrawable(
diff --git a/android/support/v7/widget/DropDownListView.java b/android/support/v7/widget/DropDownListView.java
index 5cad340..cccb82b 100644
--- a/android/support/v7/widget/DropDownListView.java
+++ b/android/support/v7/widget/DropDownListView.java
@@ -17,12 +17,23 @@
package android.support.v7.widget;
import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
import android.os.Build;
+import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.view.ViewPropertyAnimatorCompat;
import android.support.v4.widget.ListViewAutoScrollHelper;
import android.support.v7.appcompat.R;
+import android.support.v7.graphics.drawable.DrawableWrapper;
import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+import java.lang.reflect.Field;
/**
* <p>Wrapper class for a ListView. This wrapper can hijack the focus to
@@ -30,7 +41,21 @@
* displayed on screen within a drop down. The focus is never actually
* passed to the drop down in this mode; the list only looks focused.</p>
*/
-class DropDownListView extends ListViewCompat {
+class DropDownListView extends ListView {
+ public static final int INVALID_POSITION = -1;
+ public static final int NO_POSITION = -1;
+
+ private final Rect mSelectorRect = new Rect();
+ private int mSelectionLeftPadding = 0;
+ private int mSelectionTopPadding = 0;
+ private int mSelectionRightPadding = 0;
+ private int mSelectionBottomPadding = 0;
+
+ private int mMotionPosition;
+
+ private Field mIsChildViewEnabled;
+
+ private GateKeeperDrawable mSelector;
/*
* WARNING: This is a workaround for a touch mode issue.
@@ -81,10 +106,306 @@
*
* @param context this view's context
*/
- public DropDownListView(Context context, boolean hijackFocus) {
+ DropDownListView(Context context, boolean hijackFocus) {
super(context, null, R.attr.dropDownListViewStyle);
mHijackFocus = hijackFocus;
setCacheColorHint(0); // Transparent, since the background drawable could be anything.
+
+ try {
+ mIsChildViewEnabled = AbsListView.class.getDeclaredField("mIsChildViewEnabled");
+ mIsChildViewEnabled.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ }
+ }
+
+
+ @Override
+ public boolean isInTouchMode() {
+ // WARNING: Please read the comment where mListSelectionHidden is declared
+ return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
+ }
+
+ /**
+ * <p>Returns the focus state in the drop down.</p>
+ *
+ * @return true always if hijacking focus
+ */
+ @Override
+ public boolean hasWindowFocus() {
+ return mHijackFocus || super.hasWindowFocus();
+ }
+
+ /**
+ * <p>Returns the focus state in the drop down.</p>
+ *
+ * @return true always if hijacking focus
+ */
+ @Override
+ public boolean isFocused() {
+ return mHijackFocus || super.isFocused();
+ }
+
+ /**
+ * <p>Returns the focus state in the drop down.</p>
+ *
+ * @return true always if hijacking focus
+ */
+ @Override
+ public boolean hasFocus() {
+ return mHijackFocus || super.hasFocus();
+ }
+
+ @Override
+ public void setSelector(Drawable sel) {
+ mSelector = sel != null ? new GateKeeperDrawable(sel) : null;
+ super.setSelector(mSelector);
+
+ final Rect padding = new Rect();
+ if (sel != null) {
+ sel.getPadding(padding);
+ }
+
+ mSelectionLeftPadding = padding.left;
+ mSelectionTopPadding = padding.top;
+ mSelectionRightPadding = padding.right;
+ mSelectionBottomPadding = padding.bottom;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ setSelectorEnabled(true);
+ updateSelectorStateCompat();
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ final boolean drawSelectorOnTop = false;
+ if (!drawSelectorOnTop) {
+ drawSelectorCompat(canvas);
+ }
+
+ super.dispatchDraw(canvas);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mMotionPosition = pointToPosition((int) ev.getX(), (int) ev.getY());
+ break;
+ }
+ return super.onTouchEvent(ev);
+ }
+
+ /**
+ * Find a position that can be selected (i.e., is not a separator).
+ *
+ * @param position The starting position to look at.
+ * @param lookDown Whether to look down for other positions.
+ * @return The next selectable position starting at position and then searching either up or
+ * down. Returns {@link #INVALID_POSITION} if nothing can be found.
+ */
+ public int lookForSelectablePosition(int position, boolean lookDown) {
+ final ListAdapter adapter = getAdapter();
+ if (adapter == null || isInTouchMode()) {
+ return INVALID_POSITION;
+ }
+
+ final int count = adapter.getCount();
+ if (!getAdapter().areAllItemsEnabled()) {
+ if (lookDown) {
+ position = Math.max(0, position);
+ while (position < count && !adapter.isEnabled(position)) {
+ position++;
+ }
+ } else {
+ position = Math.min(position, count - 1);
+ while (position >= 0 && !adapter.isEnabled(position)) {
+ position--;
+ }
+ }
+
+ if (position < 0 || position >= count) {
+ return INVALID_POSITION;
+ }
+ return position;
+ } else {
+ if (position < 0 || position >= count) {
+ return INVALID_POSITION;
+ }
+ return position;
+ }
+ }
+
+ /**
+ * Measures the height of the given range of children (inclusive) and returns the height
+ * with this ListView's padding and divider heights included. If maxHeight is provided, the
+ * measuring will stop when the current height reaches maxHeight.
+ *
+ * @param widthMeasureSpec The width measure spec to be given to a child's
+ * {@link View#measure(int, int)}.
+ * @param startPosition The position of the first child to be shown.
+ * @param endPosition The (inclusive) position of the last child to be
+ * shown. Specify {@link #NO_POSITION} if the last child
+ * should be the last available child from the adapter.
+ * @param maxHeight The maximum height that will be returned (if all the
+ * children don't fit in this value, this value will be
+ * returned).
+ * @param disallowPartialChildPosition In general, whether the returned height should only
+ * contain entire children. This is more powerful--it is
+ * the first inclusive position at which partial
+ * children will not be allowed. Example: it looks nice
+ * to have at least 3 completely visible children, and
+ * in portrait this will most likely fit; but in
+ * landscape there could be times when even 2 children
+ * can not be completely shown, so a value of 2
+ * (remember, inclusive) would be good (assuming
+ * startPosition is 0).
+ * @return The height of this ListView with the given children.
+ */
+ public int measureHeightOfChildrenCompat(int widthMeasureSpec, int startPosition,
+ int endPosition, final int maxHeight,
+ int disallowPartialChildPosition) {
+
+ final int paddingTop = getListPaddingTop();
+ final int paddingBottom = getListPaddingBottom();
+ final int paddingLeft = getListPaddingLeft();
+ final int paddingRight = getListPaddingRight();
+ final int reportedDividerHeight = getDividerHeight();
+ final Drawable divider = getDivider();
+
+ final ListAdapter adapter = getAdapter();
+
+ if (adapter == null) {
+ return paddingTop + paddingBottom;
+ }
+
+ // Include the padding of the list
+ int returnedHeight = paddingTop + paddingBottom;
+ final int dividerHeight = ((reportedDividerHeight > 0) && divider != null)
+ ? reportedDividerHeight : 0;
+
+ // The previous height value that was less than maxHeight and contained
+ // no partial children
+ int prevHeightWithoutPartialChild = 0;
+
+ View child = null;
+ int viewType = 0;
+ int count = adapter.getCount();
+ for (int i = 0; i < count; i++) {
+ int newType = adapter.getItemViewType(i);
+ if (newType != viewType) {
+ child = null;
+ viewType = newType;
+ }
+ child = adapter.getView(i, child, this);
+
+ // Compute child height spec
+ int heightMeasureSpec;
+ ViewGroup.LayoutParams childLp = child.getLayoutParams();
+
+ if (childLp == null) {
+ childLp = generateDefaultLayoutParams();
+ child.setLayoutParams(childLp);
+ }
+
+ if (childLp.height > 0) {
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(childLp.height,
+ MeasureSpec.EXACTLY);
+ } else {
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ }
+ child.measure(widthMeasureSpec, heightMeasureSpec);
+
+ // Since this view was measured directly against the parent measure
+ // spec, we must measure it again before reuse.
+ child.forceLayout();
+
+ if (i > 0) {
+ // Count the divider for all but one child
+ returnedHeight += dividerHeight;
+ }
+
+ returnedHeight += child.getMeasuredHeight();
+
+ if (returnedHeight >= maxHeight) {
+ // We went over, figure out which height to return. If returnedHeight >
+ // maxHeight, then the i'th position did not fit completely.
+ return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
+ && (i > disallowPartialChildPosition) // We've past the min pos
+ && (prevHeightWithoutPartialChild > 0) // We have a prev height
+ && (returnedHeight != maxHeight) // i'th child did not fit completely
+ ? prevHeightWithoutPartialChild
+ : maxHeight;
+ }
+
+ if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
+ prevHeightWithoutPartialChild = returnedHeight;
+ }
+ }
+
+ // At this point, we went through the range of children, and they each
+ // completely fit, so return the returnedHeight
+ return returnedHeight;
+ }
+
+ private void setSelectorEnabled(boolean enabled) {
+ if (mSelector != null) {
+ mSelector.setEnabled(enabled);
+ }
+ }
+
+ private static class GateKeeperDrawable extends DrawableWrapper {
+ private boolean mEnabled;
+
+ GateKeeperDrawable(Drawable drawable) {
+ super(drawable);
+ mEnabled = true;
+ }
+
+ void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ @Override
+ public boolean setState(int[] stateSet) {
+ if (mEnabled) {
+ return super.setState(stateSet);
+ }
+ return false;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mEnabled) {
+ super.draw(canvas);
+ }
+ }
+
+ @Override
+ public void setHotspot(float x, float y) {
+ if (mEnabled) {
+ super.setHotspot(x, y);
+ }
+ }
+
+ @Override
+ public void setHotspotBounds(int left, int top, int right, int bottom) {
+ if (mEnabled) {
+ super.setHotspotBounds(left, top, right, bottom);
+ }
+ }
+
+ @Override
+ public boolean setVisible(boolean visible, boolean restart) {
+ if (mEnabled) {
+ return super.setVisible(visible, restart);
+ }
+ return false;
+ }
}
/**
@@ -169,6 +490,77 @@
mListSelectionHidden = hideListSelection;
}
+ private void updateSelectorStateCompat() {
+ Drawable selector = getSelector();
+ if (selector != null && touchModeDrawsInPressedStateCompat() && isPressed()) {
+ selector.setState(getDrawableState());
+ }
+ }
+
+ private void drawSelectorCompat(Canvas canvas) {
+ if (!mSelectorRect.isEmpty()) {
+ final Drawable selector = getSelector();
+ if (selector != null) {
+ selector.setBounds(mSelectorRect);
+ selector.draw(canvas);
+ }
+ }
+ }
+
+ private void positionSelectorLikeTouchCompat(int position, View sel, float x, float y) {
+ positionSelectorLikeFocusCompat(position, sel);
+
+ Drawable selector = getSelector();
+ if (selector != null && position != INVALID_POSITION) {
+ DrawableCompat.setHotspot(selector, x, y);
+ }
+ }
+
+ private void positionSelectorLikeFocusCompat(int position, View sel) {
+ // If we're changing position, update the visibility since the selector
+ // is technically being detached from the previous selection.
+ final Drawable selector = getSelector();
+ final boolean manageState = selector != null && position != INVALID_POSITION;
+ if (manageState) {
+ selector.setVisible(false, false);
+ }
+
+ positionSelectorCompat(position, sel);
+
+ if (manageState) {
+ final Rect bounds = mSelectorRect;
+ final float x = bounds.exactCenterX();
+ final float y = bounds.exactCenterY();
+ selector.setVisible(getVisibility() == VISIBLE, false);
+ DrawableCompat.setHotspot(selector, x, y);
+ }
+ }
+
+ private void positionSelectorCompat(int position, View sel) {
+ final Rect selectorRect = mSelectorRect;
+ selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), sel.getBottom());
+
+ // Adjust for selection padding.
+ selectorRect.left -= mSelectionLeftPadding;
+ selectorRect.top -= mSelectionTopPadding;
+ selectorRect.right += mSelectionRightPadding;
+ selectorRect.bottom += mSelectionBottomPadding;
+
+ try {
+ // AbsListView.mIsChildViewEnabled controls the selector's state so we need to
+ // modify its value
+ final boolean isChildViewEnabled = mIsChildViewEnabled.getBoolean(this);
+ if (sel.isEnabled() != isChildViewEnabled) {
+ mIsChildViewEnabled.set(this, !isChildViewEnabled);
+ if (position != INVALID_POSITION) {
+ refreshDrawableState();
+ }
+ }
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
private void clearPressedItem() {
mDrawsInPressedState = false;
setPressed(false);
@@ -233,44 +625,7 @@
refreshDrawableState();
}
- @Override
- protected boolean touchModeDrawsInPressedStateCompat() {
- return mDrawsInPressedState || super.touchModeDrawsInPressedStateCompat();
- }
-
- @Override
- public boolean isInTouchMode() {
- // WARNING: Please read the comment where mListSelectionHidden is declared
- return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
- }
-
- /**
- * <p>Returns the focus state in the drop down.</p>
- *
- * @return true always if hijacking focus
- */
- @Override
- public boolean hasWindowFocus() {
- return mHijackFocus || super.hasWindowFocus();
- }
-
- /**
- * <p>Returns the focus state in the drop down.</p>
- *
- * @return true always if hijacking focus
- */
- @Override
- public boolean isFocused() {
- return mHijackFocus || super.isFocused();
- }
-
- /**
- * <p>Returns the focus state in the drop down.</p>
- *
- * @return true always if hijacking focus
- */
- @Override
- public boolean hasFocus() {
- return mHijackFocus || super.hasFocus();
+ private boolean touchModeDrawsInPressedStateCompat() {
+ return mDrawsInPressedState;
}
}
diff --git a/android/support/v7/widget/LinearLayoutCompat.java b/android/support/v7/widget/LinearLayoutCompat.java
index f071ae4..ef68896 100644
--- a/android/support/v7/widget/LinearLayoutCompat.java
+++ b/android/support/v7/widget/LinearLayoutCompat.java
@@ -16,6 +16,7 @@
package android.support.v7.widget;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.content.Context;
@@ -559,6 +560,7 @@
* @return true if there should be a divider before the child at childIndex
* @hide Pending API consideration. Currently only used internally by the system.
*/
+ @RestrictTo(LIBRARY)
protected boolean hasDividerBeforeChildAt(int childIndex) {
if (childIndex == 0) {
return (mShowDividers & SHOW_DIVIDER_BEGINNING) != 0;
diff --git a/android/support/v7/widget/ListViewCompat.java b/android/support/v7/widget/ListViewCompat.java
deleted file mode 100644
index 3a2fba3..0000000
--- a/android/support/v7/widget/ListViewCompat.java
+++ /dev/null
@@ -1,413 +0,0 @@
-/*
- * Copyright (C) 2014 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.support.v7.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.RestrictTo;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import android.support.v7.graphics.drawable.DrawableWrapper;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AbsListView;
-import android.widget.ListAdapter;
-import android.widget.ListView;
-
-import java.lang.reflect.Field;
-
-/**
- * This class contains a number of useful things for ListView. Mainly used by
- * {@link android.support.v7.widget.ListPopupWindow}.
- *
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class ListViewCompat extends ListView {
-
- public static final int INVALID_POSITION = -1;
- public static final int NO_POSITION = -1;
-
- private static final int[] STATE_SET_NOTHING = new int[] { 0 };
-
- final Rect mSelectorRect = new Rect();
- int mSelectionLeftPadding = 0;
- int mSelectionTopPadding = 0;
- int mSelectionRightPadding = 0;
- int mSelectionBottomPadding = 0;
-
- protected int mMotionPosition;
-
- private Field mIsChildViewEnabled;
-
- private GateKeeperDrawable mSelector;
-
- public ListViewCompat(Context context) {
- this(context, null);
- }
-
- public ListViewCompat(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public ListViewCompat(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- try {
- mIsChildViewEnabled = AbsListView.class.getDeclaredField("mIsChildViewEnabled");
- mIsChildViewEnabled.setAccessible(true);
- } catch (NoSuchFieldException e) {
- e.printStackTrace();
- }
- }
-
- @Override
- public void setSelector(Drawable sel) {
- mSelector = sel != null ? new GateKeeperDrawable(sel) : null;
- super.setSelector(mSelector);
-
- final Rect padding = new Rect();
- if (sel != null) {
- sel.getPadding(padding);
- }
-
- mSelectionLeftPadding = padding.left;
- mSelectionTopPadding = padding.top;
- mSelectionRightPadding = padding.right;
- mSelectionBottomPadding = padding.bottom;
- }
-
- @Override
- protected void drawableStateChanged() {
- super.drawableStateChanged();
-
- setSelectorEnabled(true);
- updateSelectorStateCompat();
- }
-
- @Override
- protected void dispatchDraw(Canvas canvas) {
- final boolean drawSelectorOnTop = false;
- if (!drawSelectorOnTop) {
- drawSelectorCompat(canvas);
- }
-
- super.dispatchDraw(canvas);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- switch (ev.getAction()) {
- case MotionEvent.ACTION_DOWN:
- mMotionPosition = pointToPosition((int) ev.getX(), (int) ev.getY());
- break;
- }
- return super.onTouchEvent(ev);
- }
-
- protected void updateSelectorStateCompat() {
- Drawable selector = getSelector();
- if (selector != null && shouldShowSelectorCompat()) {
- selector.setState(getDrawableState());
- }
- }
-
- protected boolean shouldShowSelectorCompat() {
- return touchModeDrawsInPressedStateCompat() && isPressed();
- }
-
- protected boolean touchModeDrawsInPressedStateCompat() {
- return false;
- }
-
- protected void drawSelectorCompat(Canvas canvas) {
- if (!mSelectorRect.isEmpty()) {
- final Drawable selector = getSelector();
- if (selector != null) {
- selector.setBounds(mSelectorRect);
- selector.draw(canvas);
- }
- }
- }
-
- /**
- * Find a position that can be selected (i.e., is not a separator).
- *
- * @param position The starting position to look at.
- * @param lookDown Whether to look down for other positions.
- * @return The next selectable position starting at position and then searching either up or
- * down. Returns {@link #INVALID_POSITION} if nothing can be found.
- */
- public int lookForSelectablePosition(int position, boolean lookDown) {
- final ListAdapter adapter = getAdapter();
- if (adapter == null || isInTouchMode()) {
- return INVALID_POSITION;
- }
-
- final int count = adapter.getCount();
- if (!getAdapter().areAllItemsEnabled()) {
- if (lookDown) {
- position = Math.max(0, position);
- while (position < count && !adapter.isEnabled(position)) {
- position++;
- }
- } else {
- position = Math.min(position, count - 1);
- while (position >= 0 && !adapter.isEnabled(position)) {
- position--;
- }
- }
-
- if (position < 0 || position >= count) {
- return INVALID_POSITION;
- }
- return position;
- } else {
- if (position < 0 || position >= count) {
- return INVALID_POSITION;
- }
- return position;
- }
- }
-
- protected void positionSelectorLikeTouchCompat(int position, View sel, float x, float y) {
- positionSelectorLikeFocusCompat(position, sel);
-
- Drawable selector = getSelector();
- if (selector != null && position != INVALID_POSITION) {
- DrawableCompat.setHotspot(selector, x, y);
- }
- }
-
- protected void positionSelectorLikeFocusCompat(int position, View sel) {
- // If we're changing position, update the visibility since the selector
- // is technically being detached from the previous selection.
- final Drawable selector = getSelector();
- final boolean manageState = selector != null && position != INVALID_POSITION;
- if (manageState) {
- selector.setVisible(false, false);
- }
-
- positionSelectorCompat(position, sel);
-
- if (manageState) {
- final Rect bounds = mSelectorRect;
- final float x = bounds.exactCenterX();
- final float y = bounds.exactCenterY();
- selector.setVisible(getVisibility() == VISIBLE, false);
- DrawableCompat.setHotspot(selector, x, y);
- }
- }
-
- protected void positionSelectorCompat(int position, View sel) {
- final Rect selectorRect = mSelectorRect;
- selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), sel.getBottom());
-
- // Adjust for selection padding.
- selectorRect.left -= mSelectionLeftPadding;
- selectorRect.top -= mSelectionTopPadding;
- selectorRect.right += mSelectionRightPadding;
- selectorRect.bottom += mSelectionBottomPadding;
-
- try {
- // AbsListView.mIsChildViewEnabled controls the selector's state so we need to
- // modify its value
- final boolean isChildViewEnabled = mIsChildViewEnabled.getBoolean(this);
- if (sel.isEnabled() != isChildViewEnabled) {
- mIsChildViewEnabled.set(this, !isChildViewEnabled);
- if (position != INVALID_POSITION) {
- refreshDrawableState();
- }
- }
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- }
- }
-
- /**
- * Measures the height of the given range of children (inclusive) and returns the height
- * with this ListView's padding and divider heights included. If maxHeight is provided, the
- * measuring will stop when the current height reaches maxHeight.
- *
- * @param widthMeasureSpec The width measure spec to be given to a child's
- * {@link View#measure(int, int)}.
- * @param startPosition The position of the first child to be shown.
- * @param endPosition The (inclusive) position of the last child to be
- * shown. Specify {@link #NO_POSITION} if the last child
- * should be the last available child from the adapter.
- * @param maxHeight The maximum height that will be returned (if all the
- * children don't fit in this value, this value will be
- * returned).
- * @param disallowPartialChildPosition In general, whether the returned height should only
- * contain entire children. This is more powerful--it is
- * the first inclusive position at which partial
- * children will not be allowed. Example: it looks nice
- * to have at least 3 completely visible children, and
- * in portrait this will most likely fit; but in
- * landscape there could be times when even 2 children
- * can not be completely shown, so a value of 2
- * (remember, inclusive) would be good (assuming
- * startPosition is 0).
- * @return The height of this ListView with the given children.
- */
- public int measureHeightOfChildrenCompat(int widthMeasureSpec, int startPosition,
- int endPosition, final int maxHeight,
- int disallowPartialChildPosition) {
-
- final int paddingTop = getListPaddingTop();
- final int paddingBottom = getListPaddingBottom();
- final int paddingLeft = getListPaddingLeft();
- final int paddingRight = getListPaddingRight();
- final int reportedDividerHeight = getDividerHeight();
- final Drawable divider = getDivider();
-
- final ListAdapter adapter = getAdapter();
-
- if (adapter == null) {
- return paddingTop + paddingBottom;
- }
-
- // Include the padding of the list
- int returnedHeight = paddingTop + paddingBottom;
- final int dividerHeight = ((reportedDividerHeight > 0) && divider != null)
- ? reportedDividerHeight : 0;
-
- // The previous height value that was less than maxHeight and contained
- // no partial children
- int prevHeightWithoutPartialChild = 0;
-
- View child = null;
- int viewType = 0;
- int count = adapter.getCount();
- for (int i = 0; i < count; i++) {
- int newType = adapter.getItemViewType(i);
- if (newType != viewType) {
- child = null;
- viewType = newType;
- }
- child = adapter.getView(i, child, this);
-
- // Compute child height spec
- int heightMeasureSpec;
- ViewGroup.LayoutParams childLp = child.getLayoutParams();
-
- if (childLp == null) {
- childLp = generateDefaultLayoutParams();
- child.setLayoutParams(childLp);
- }
-
- if (childLp.height > 0) {
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(childLp.height,
- MeasureSpec.EXACTLY);
- } else {
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
- }
- child.measure(widthMeasureSpec, heightMeasureSpec);
-
- // Since this view was measured directly against the parent measure
- // spec, we must measure it again before reuse.
- child.forceLayout();
-
- if (i > 0) {
- // Count the divider for all but one child
- returnedHeight += dividerHeight;
- }
-
- returnedHeight += child.getMeasuredHeight();
-
- if (returnedHeight >= maxHeight) {
- // We went over, figure out which height to return. If returnedHeight >
- // maxHeight, then the i'th position did not fit completely.
- return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
- && (i > disallowPartialChildPosition) // We've past the min pos
- && (prevHeightWithoutPartialChild > 0) // We have a prev height
- && (returnedHeight != maxHeight) // i'th child did not fit completely
- ? prevHeightWithoutPartialChild
- : maxHeight;
- }
-
- if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
- prevHeightWithoutPartialChild = returnedHeight;
- }
- }
-
- // At this point, we went through the range of children, and they each
- // completely fit, so return the returnedHeight
- return returnedHeight;
- }
-
- protected void setSelectorEnabled(boolean enabled) {
- if (mSelector != null) {
- mSelector.setEnabled(enabled);
- }
- }
-
- private static class GateKeeperDrawable extends DrawableWrapper {
- private boolean mEnabled;
-
- public GateKeeperDrawable(Drawable drawable) {
- super(drawable);
- mEnabled = true;
- }
-
- void setEnabled(boolean enabled) {
- mEnabled = enabled;
- }
-
- @Override
- public boolean setState(int[] stateSet) {
- if (mEnabled) {
- return super.setState(stateSet);
- }
- return false;
- }
-
- @Override
- public void draw(Canvas canvas) {
- if (mEnabled) {
- super.draw(canvas);
- }
- }
-
- @Override
- public void setHotspot(float x, float y) {
- if (mEnabled) {
- super.setHotspot(x, y);
- }
- }
-
- @Override
- public void setHotspotBounds(int left, int top, int right, int bottom) {
- if (mEnabled) {
- super.setHotspotBounds(left, top, right, bottom);
- }
- }
-
- @Override
- public boolean setVisible(boolean visible, boolean restart) {
- if (mEnabled) {
- return super.setVisible(visible, restart);
- }
- return false;
- }
- }
-}
diff --git a/android/support/v7/widget/RecyclerView.java b/android/support/v7/widget/RecyclerView.java
index a287979..b195d3c 100644
--- a/android/support/v7/widget/RecyclerView.java
+++ b/android/support/v7/widget/RecyclerView.java
@@ -2722,7 +2722,7 @@
removeCallbacks(mItemAnimatorRunner);
mViewInfoStore.onDetach();
- if (ALLOW_THREAD_GAP_WORK) {
+ if (ALLOW_THREAD_GAP_WORK && mGapWorker != null) {
// Unregister with gap worker
mGapWorker.remove(this);
mGapWorker = null;
@@ -6643,11 +6643,19 @@
* @see #onCreateViewHolder(ViewGroup, int)
*/
public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
- TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
- final VH holder = onCreateViewHolder(parent, viewType);
- holder.mItemViewType = viewType;
- TraceCompat.endSection();
- return holder;
+ try {
+ TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
+ final VH holder = onCreateViewHolder(parent, viewType);
+ if (holder.itemView.getParent() != null) {
+ throw new IllegalStateException("ViewHolder views must not be attached when"
+ + " created. Ensure that you are not passing 'true' to the attachToRoot"
+ + " parameter of LayoutInflater.inflate(..., boolean attachToRoot)");
+ }
+ holder.mItemViewType = viewType;
+ return holder;
+ } finally {
+ TraceCompat.endSection();
+ }
}
/**
@@ -10108,7 +10116,7 @@
if (vScroll == 0 && hScroll == 0) {
return false;
}
- mRecyclerView.scrollBy(hScroll, vScroll);
+ mRecyclerView.smoothScrollBy(hScroll, vScroll);
return true;
}
diff --git a/android/support/v7/widget/StaggeredGridLayoutManager.java b/android/support/v7/widget/StaggeredGridLayoutManager.java
index 55fb14e..4e560b4 100644
--- a/android/support/v7/widget/StaggeredGridLayoutManager.java
+++ b/android/support/v7/widget/StaggeredGridLayoutManager.java
@@ -16,6 +16,7 @@
package android.support.v7.widget;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_HEAD;
import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_TAIL;
@@ -2069,6 +2070,7 @@
/** @hide */
@Override
+ @RestrictTo(LIBRARY)
public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
LayoutPrefetchRegistry layoutPrefetchRegistry) {
/* This method uses the simplifying assumption that the next N items (where N = span count)
diff --git a/android/support/v7/widget/TooltipCompatHandler.java b/android/support/v7/widget/TooltipCompatHandler.java
index 63a6198..8de44e6 100644
--- a/android/support/v7/widget/TooltipCompatHandler.java
+++ b/android/support/v7/widget/TooltipCompatHandler.java
@@ -22,6 +22,7 @@
import android.content.Context;
import android.support.annotation.RestrictTo;
import android.support.v4.view.ViewCompat;
+import android.support.v4.view.ViewConfigurationCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.MotionEvent;
@@ -46,6 +47,7 @@
private final View mAnchor;
private final CharSequence mTooltipText;
+ private final int mHoverSlop;
private final Runnable mShowRunnable = new Runnable() {
@Override
@@ -104,6 +106,9 @@
private TooltipCompatHandler(View anchor, CharSequence tooltipText) {
mAnchor = anchor;
mTooltipText = tooltipText;
+ mHoverSlop = ViewConfigurationCompat.getScaledHoverSlop(
+ ViewConfiguration.get(mAnchor.getContext()));
+ clearAnchorPos();
mAnchor.setOnLongClickListener(this);
mAnchor.setOnHoverListener(this);
@@ -129,13 +134,12 @@
}
switch (event.getAction()) {
case MotionEvent.ACTION_HOVER_MOVE:
- if (mAnchor.isEnabled() && mPopup == null) {
- mAnchorX = (int) event.getX();
- mAnchorY = (int) event.getY();
+ if (mAnchor.isEnabled() && mPopup == null && updateAnchorPos(event)) {
setPendingHandler(this);
}
break;
case MotionEvent.ACTION_HOVER_EXIT:
+ clearAnchorPos();
hide();
break;
}
@@ -188,6 +192,7 @@
if (mPopup != null) {
mPopup.hide();
mPopup = null;
+ clearAnchorPos();
mAnchor.removeOnAttachStateChangeListener(this);
} else {
Log.e(TAG, "sActiveHandler.mPopup == null");
@@ -216,4 +221,31 @@
private void cancelPendingShow() {
mAnchor.removeCallbacks(mShowRunnable);
}
+
+ /**
+ * Update the anchor position if it significantly (that is by at least mHoverSlope)
+ * different from the previously stored position. Ignoring insignificant changes
+ * filters out the jitter which is typical for such input sources as stylus.
+ *
+ * @return True if the position has been updated.
+ */
+ private boolean updateAnchorPos(MotionEvent event) {
+ final int newAnchorX = (int) event.getX();
+ final int newAnchorY = (int) event.getY();
+ if (Math.abs(newAnchorX - mAnchorX) <= mHoverSlop
+ && Math.abs(newAnchorY - mAnchorY) <= mHoverSlop) {
+ return false;
+ }
+ mAnchorX = newAnchorX;
+ mAnchorY = newAnchorY;
+ return true;
+ }
+
+ /**
+ * Clear the anchor position to ensure that the next change is considered significant.
+ */
+ private void clearAnchorPos() {
+ mAnchorX = Integer.MAX_VALUE;
+ mAnchorY = Integer.MAX_VALUE;
+ }
}
diff --git a/android/support/wear/widget/BoxInsetLayout.java b/android/support/wear/widget/BoxInsetLayout.java
index a8b1381..383bcb7 100644
--- a/android/support/wear/widget/BoxInsetLayout.java
+++ b/android/support/wear/widget/BoxInsetLayout.java
@@ -46,7 +46,7 @@
@UiThread
public class BoxInsetLayout extends ViewGroup {
- private static final float FACTOR = 0.146467f; //(1 - sqrt(2)/2)/2
+ private static final float FACTOR = 0.146447f; //(1 - sqrt(2)/2)/2
private static final int DEFAULT_CHILD_GRAVITY = Gravity.TOP | Gravity.START;
private final int mScreenHeight;
diff --git a/android/system/Os.java b/android/system/Os.java
index cc24cc5..a4b90e3 100644
--- a/android/system/Os.java
+++ b/android/system/Os.java
@@ -280,12 +280,7 @@
/** @hide */ public static int ioctlInt(FileDescriptor fd, int cmd, Int32Ref arg) throws ErrnoException {
- libcore.util.MutableInt internalArg = new libcore.util.MutableInt(arg.value);
- try {
- return Libcore.os.ioctlInt(fd, cmd, internalArg);
- } finally {
- arg.value = internalArg.value;
- }
+ return Libcore.os.ioctlInt(fd, cmd, arg);
}
/**
@@ -472,18 +467,8 @@
/**
* See <a href="http://man7.org/linux/man-pages/man2/sendfile.2.html">sendfile(2)</a>.
*/
- public static long sendfile(FileDescriptor outFd, FileDescriptor inFd, Int64Ref inOffset, long byteCount) throws ErrnoException {
- if (inOffset == null) {
- return Libcore.os.sendfile(outFd, inFd, null, byteCount);
- } else {
- libcore.util.MutableLong internalInOffset = new libcore.util.MutableLong(
- inOffset.value);
- try {
- return Libcore.os.sendfile(outFd, inFd, internalInOffset, byteCount);
- } finally {
- inOffset.value = internalInOffset.value;
- }
- }
+ public static long sendfile(FileDescriptor outFd, FileDescriptor inFd, Int64Ref offset, long byteCount) throws ErrnoException {
+ return Libcore.os.sendfile(outFd, inFd, offset, byteCount);
}
/**
@@ -587,6 +572,12 @@
public static void socketpair(int domain, int type, int protocol, FileDescriptor fd1, FileDescriptor fd2) throws ErrnoException { Libcore.os.socketpair(domain, type, protocol, fd1, fd2); }
/**
+ * See <a href="http://man7.org/linux/man-pages/man2/splice.2.html">splice(2)</a>.
+ * @hide
+ */
+ public static long splice(FileDescriptor fdIn, Int64Ref offIn, FileDescriptor fdOut, Int64Ref offOut, long len, int flags) throws ErrnoException { return Libcore.os.splice(fdIn, offIn, fdOut, offOut, len, flags); }
+
+ /**
* See <a href="http://man7.org/linux/man-pages/man2/stat.2.html">stat(2)</a>.
*/
public static StructStat stat(String path) throws ErrnoException { return Libcore.os.stat(path); }
@@ -652,16 +643,7 @@
* @throws IllegalArgumentException if {@code status != null && status.length != 1}
*/
public static int waitpid(int pid, Int32Ref status, int options) throws ErrnoException {
- if (status == null) {
- return Libcore.os.waitpid(pid, null, options);
- } else {
- libcore.util.MutableInt internalStatus = new libcore.util.MutableInt(status.value);
- try {
- return Libcore.os.waitpid(pid, internalStatus, options);
- } finally {
- status.value = internalStatus.value;
- }
- }
+ return Libcore.os.waitpid(pid, status, options);
}
/**
diff --git a/android/system/OsConstants.java b/android/system/OsConstants.java
index 83a1b41..1b8c2ff 100644
--- a/android/system/OsConstants.java
+++ b/android/system/OsConstants.java
@@ -486,6 +486,9 @@
public static final int SO_SNDLOWAT = placeholder();
public static final int SO_SNDTIMEO = placeholder();
public static final int SO_TYPE = placeholder();
+ /** @hide */ public static final int SPLICE_F_MOVE = placeholder();
+ /** @hide */ public static final int SPLICE_F_NONBLOCK = placeholder();
+ /** @hide */ public static final int SPLICE_F_MORE = placeholder();
public static final int STDERR_FILENO = placeholder();
public static final int STDIN_FILENO = placeholder();
public static final int STDOUT_FILENO = placeholder();
diff --git a/android/telecom/Call.java b/android/telecom/Call.java
index 2091101..6799417 100644
--- a/android/telecom/Call.java
+++ b/android/telecom/Call.java
@@ -419,7 +419,6 @@
/**
* Indicates the call used Assisted Dialing.
* See also {@link Connection#PROPERTY_ASSISTED_DIALING_USED}
- * @hide
*/
public static final int PROPERTY_ASSISTED_DIALING_USED = 0x00000200;
@@ -1408,7 +1407,7 @@
* @param extras Bundle containing extra information associated with the event.
*/
public void sendCallEvent(String event, Bundle extras) {
- mInCallAdapter.sendCallEvent(mTelecomCallId, event, extras);
+ mInCallAdapter.sendCallEvent(mTelecomCallId, event, mTargetSdkVersion, extras);
}
/**
@@ -1961,6 +1960,15 @@
}
}
+ /** {@hide} */
+ final void internalOnHandoverComplete() {
+ for (CallbackRecord<Callback> record : mCallbackRecords) {
+ final Call call = this;
+ final Callback callback = record.getCallback();
+ record.getHandler().post(() -> callback.onHandoverComplete(call));
+ }
+ }
+
private void fireStateChanged(final int newState) {
for (CallbackRecord<Callback> record : mCallbackRecords) {
final Call call = this;
diff --git a/android/telecom/Connection.java b/android/telecom/Connection.java
index aaef8d3..63f970a 100644
--- a/android/telecom/Connection.java
+++ b/android/telecom/Connection.java
@@ -35,6 +35,7 @@
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.ArraySet;
@@ -401,7 +402,6 @@
/**
* Set by the framework to indicate that a connection is using assisted dialing.
- * @hide
*/
public static final int PROPERTY_ASSISTED_DIALING_USED = 1 << 9;
@@ -2538,6 +2538,19 @@
}
/**
+ * Adds a parcelable extra to this {@code Connection}.
+ *
+ * @param key The extra key.
+ * @param value The value.
+ * @hide
+ */
+ public final void putExtra(@NonNull String key, @NonNull Parcelable value) {
+ Bundle newExtras = new Bundle();
+ newExtras.putParcelable(key, value);
+ putExtras(newExtras);
+ }
+
+ /**
* Removes extras from this {@code Connection}.
*
* @param keys The keys of the extras to remove.
@@ -2788,6 +2801,15 @@
public void onCallEvent(String event, Bundle extras) {}
/**
+ * Notifies this {@link Connection} that a handover has completed.
+ * <p>
+ * A handover is initiated with {@link android.telecom.Call#handoverTo(PhoneAccountHandle, int,
+ * Bundle)} on the initiating side of the handover, and
+ * {@link TelecomManager#acceptHandover(Uri, int, PhoneAccountHandle)}.
+ */
+ public void onHandoverComplete() {}
+
+ /**
* Notifies this {@link Connection} of a change to the extras made outside the
* {@link ConnectionService}.
* <p>
diff --git a/android/telecom/ConnectionService.java b/android/telecom/ConnectionService.java
index 6af01ae..c1040ad 100644
--- a/android/telecom/ConnectionService.java
+++ b/android/telecom/ConnectionService.java
@@ -140,6 +140,7 @@
private static final String SESSION_POST_DIAL_CONT = "CS.oPDC";
private static final String SESSION_PULL_EXTERNAL_CALL = "CS.pEC";
private static final String SESSION_SEND_CALL_EVENT = "CS.sCE";
+ private static final String SESSION_HANDOVER_COMPLETE = "CS.hC";
private static final String SESSION_EXTRAS_CHANGED = "CS.oEC";
private static final String SESSION_START_RTT = "CS.+RTT";
private static final String SESSION_STOP_RTT = "CS.-RTT";
@@ -179,6 +180,7 @@
private static final int MSG_CONNECTION_SERVICE_FOCUS_LOST = 30;
private static final int MSG_CONNECTION_SERVICE_FOCUS_GAINED = 31;
private static final int MSG_HANDOVER_FAILED = 32;
+ private static final int MSG_HANDOVER_COMPLETE = 33;
private static Connection sNullConnection;
@@ -298,6 +300,19 @@
}
@Override
+ public void handoverComplete(String callId, Session.Info sessionInfo) {
+ Log.startSession(sessionInfo, SESSION_HANDOVER_COMPLETE);
+ try {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.arg2 = Log.createSubsession();
+ mHandler.obtainMessage(MSG_HANDOVER_COMPLETE, args).sendToTarget();
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
public void abort(String callId, Session.Info sessionInfo) {
Log.startSession(sessionInfo, SESSION_ABORT);
try {
@@ -1028,6 +1043,19 @@
}
break;
}
+ case MSG_HANDOVER_COMPLETE: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ Log.continueSession((Session) args.arg2,
+ SESSION_HANDLER + SESSION_HANDOVER_COMPLETE);
+ String callId = (String) args.arg1;
+ notifyHandoverComplete(callId);
+ } finally {
+ args.recycle();
+ Log.endSession();
+ }
+ break;
+ }
case MSG_ON_EXTRAS_CHANGED: {
SomeArgs args = (SomeArgs) msg.obj;
try {
@@ -1445,19 +1473,24 @@
final ConnectionRequest request,
boolean isIncoming,
boolean isUnknown) {
+ boolean isLegacyHandover = request.getExtras() != null &&
+ request.getExtras().getBoolean(TelecomManager.EXTRA_IS_HANDOVER, false);
+ boolean isHandover = request.getExtras() != null && request.getExtras().getBoolean(
+ TelecomManager.EXTRA_IS_HANDOVER_CONNECTION, false);
Log.d(this, "createConnection, callManagerAccount: %s, callId: %s, request: %s, " +
- "isIncoming: %b, isUnknown: %b", callManagerAccount, callId, request,
- isIncoming,
- isUnknown);
+ "isIncoming: %b, isUnknown: %b, isLegacyHandover: %b, isHandover: %b",
+ callManagerAccount, callId, request, isIncoming, isUnknown, isLegacyHandover,
+ isHandover);
Connection connection = null;
- if (getApplicationContext().getApplicationInfo().targetSdkVersion >
- Build.VERSION_CODES.O_MR1 && request.getExtras() != null &&
- request.getExtras().getBoolean(TelecomManager.EXTRA_IS_HANDOVER,false)) {
+ if (isHandover) {
+ PhoneAccountHandle fromPhoneAccountHandle = request.getExtras() != null
+ ? (PhoneAccountHandle) request.getExtras().getParcelable(
+ TelecomManager.EXTRA_HANDOVER_FROM_PHONE_ACCOUNT) : null;
if (!isIncoming) {
- connection = onCreateOutgoingHandoverConnection(callManagerAccount, request);
+ connection = onCreateOutgoingHandoverConnection(fromPhoneAccountHandle, request);
} else {
- connection = onCreateIncomingHandoverConnection(callManagerAccount, request);
+ connection = onCreateIncomingHandoverConnection(fromPhoneAccountHandle, request);
}
} else {
connection = isUnknown ? onCreateUnknownConnection(callManagerAccount, request)
@@ -1754,6 +1787,19 @@
}
/**
+ * Notifies a {@link Connection} that a handover has completed.
+ *
+ * @param callId The ID of the call which completed handover.
+ */
+ private void notifyHandoverComplete(String callId) {
+ Log.d(this, "notifyHandoverComplete(%s)", callId);
+ Connection connection = findConnectionForAction(callId, "notifyHandoverComplete");
+ if (connection != null) {
+ connection.onHandoverComplete();
+ }
+ }
+
+ /**
* Notifies a {@link Connection} or {@link Conference} of a change to the extras from Telecom.
* <p>
* These extra changes can originate from Telecom itself, or from an {@link InCallService} via
diff --git a/android/telecom/InCallAdapter.java b/android/telecom/InCallAdapter.java
index 4bc2a9b..658685f 100644
--- a/android/telecom/InCallAdapter.java
+++ b/android/telecom/InCallAdapter.java
@@ -286,11 +286,12 @@
*
* @param callId The callId to send the event for.
* @param event The event.
+ * @param targetSdkVer Target sdk version of the app calling this api
* @param extras Extras associated with the event.
*/
- public void sendCallEvent(String callId, String event, Bundle extras) {
+ public void sendCallEvent(String callId, String event, int targetSdkVer, Bundle extras) {
try {
- mAdapter.sendCallEvent(callId, event, extras);
+ mAdapter.sendCallEvent(callId, event, targetSdkVer, extras);
} catch (RemoteException ignored) {
}
}
diff --git a/android/telecom/InCallService.java b/android/telecom/InCallService.java
index 74fa62d..fcf04c9 100644
--- a/android/telecom/InCallService.java
+++ b/android/telecom/InCallService.java
@@ -81,6 +81,7 @@
private static final int MSG_ON_RTT_UPGRADE_REQUEST = 10;
private static final int MSG_ON_RTT_INITIATION_FAILURE = 11;
private static final int MSG_ON_HANDOVER_FAILED = 12;
+ private static final int MSG_ON_HANDOVER_COMPLETE = 13;
/** Default Handler used to consolidate binder method calls onto a single thread. */
private final Handler mHandler = new Handler(Looper.getMainLooper()) {
@@ -157,6 +158,11 @@
mPhone.internalOnHandoverFailed(callId, error);
break;
}
+ case MSG_ON_HANDOVER_COMPLETE: {
+ String callId = (String) msg.obj;
+ mPhone.internalOnHandoverComplete(callId);
+ break;
+ }
default:
break;
}
@@ -237,6 +243,11 @@
public void onHandoverFailed(String callId, int error) {
mHandler.obtainMessage(MSG_ON_HANDOVER_FAILED, error, 0, callId).sendToTarget();
}
+
+ @Override
+ public void onHandoverComplete(String callId) {
+ mHandler.obtainMessage(MSG_ON_HANDOVER_COMPLETE, callId).sendToTarget();
+ }
}
private Phone.Listener mPhoneListener = new Phone.Listener() {
diff --git a/android/telecom/Log.java b/android/telecom/Log.java
index 3361b5b..83ca470 100644
--- a/android/telecom/Log.java
+++ b/android/telecom/Log.java
@@ -340,24 +340,6 @@
return sSessionManager;
}
- private static MessageDigest sMessageDigest;
-
- public static void initMd5Sum() {
- new AsyncTask<Void, Void, Void>() {
- @Override
- public Void doInBackground(Void... args) {
- MessageDigest md;
- try {
- md = MessageDigest.getInstance("SHA-1");
- } catch (NoSuchAlgorithmException e) {
- md = null;
- }
- sMessageDigest = md;
- return null;
- }
- }.execute();
- }
-
public static void setTag(String tag) {
TAG = tag;
DEBUG = isLoggable(android.util.Log.DEBUG);
@@ -425,44 +407,13 @@
/**
* Redact personally identifiable information for production users.
* If we are running in verbose mode, return the original string,
- * and return "****" if we are running on the user build, otherwise
- * return a SHA-1 hash of the input string.
+ * and return "***" otherwise.
*/
public static String pii(Object pii) {
if (pii == null || VERBOSE) {
return String.valueOf(pii);
}
- return "[" + secureHash(String.valueOf(pii).getBytes()) + "]";
- }
-
- private static String secureHash(byte[] input) {
- // Refrain from logging user personal information in user build.
- if (USER_BUILD) {
- return "****";
- }
-
- if (sMessageDigest != null) {
- sMessageDigest.reset();
- sMessageDigest.update(input);
- byte[] result = sMessageDigest.digest();
- return encodeHex(result);
- } else {
- return "Uninitialized SHA1";
- }
- }
-
- private static String encodeHex(byte[] bytes) {
- StringBuffer hex = new StringBuffer(bytes.length * 2);
-
- for (int i = 0; i < bytes.length; i++) {
- int byteIntValue = bytes[i] & 0xff;
- if (byteIntValue < 0x10) {
- hex.append("0");
- }
- hex.append(Integer.toString(byteIntValue, 16));
- }
-
- return hex.toString();
+ return "***";
}
private static String getPrefixFromObject(Object obj) {
diff --git a/android/telecom/Phone.java b/android/telecom/Phone.java
index b5394b9..99f94f2 100644
--- a/android/telecom/Phone.java
+++ b/android/telecom/Phone.java
@@ -230,6 +230,13 @@
}
}
+ final void internalOnHandoverComplete(String callId) {
+ Call call = mCallByTelecomCallId.get(callId);
+ if (call != null) {
+ call.internalOnHandoverComplete();
+ }
+ }
+
/**
* Called to destroy the phone and cleanup any lingering calls.
*/
diff --git a/android/telecom/TelecomManager.java b/android/telecom/TelecomManager.java
index 15355ac..1fe5db5 100644
--- a/android/telecom/TelecomManager.java
+++ b/android/telecom/TelecomManager.java
@@ -24,6 +24,7 @@
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -110,6 +111,12 @@
"android.telecom.action.SHOW_RESPOND_VIA_SMS_SETTINGS";
/**
+ * The {@link android.content.Intent} action used to show the assisted dialing settings.
+ */
+ public static final String ACTION_SHOW_ASSISTED_DIALING_SETTINGS =
+ "android.telecom.action.SHOW_ASSISTED_DIALING_SETTINGS";
+
+ /**
* The {@link android.content.Intent} action used to show the settings page used to configure
* {@link PhoneAccount} preferences.
*/
@@ -236,6 +243,15 @@
"android.telecom.extra.INCOMING_CALL_EXTRAS";
/**
+ * Optional extra for {@link #ACTION_INCOMING_CALL} containing a boolean to indicate that the
+ * call has an externally generated ringer. Used by the HfpClientConnectionService when In Band
+ * Ringtone is enabled to prevent two ringers from being generated.
+ * @hide
+ */
+ public static final String EXTRA_CALL_EXTERNAL_RINGER =
+ "android.telecom.extra.CALL_EXTERNAL_RINGER";
+
+ /**
* Optional extra for {@link android.content.Intent#ACTION_CALL} and
* {@link android.content.Intent#ACTION_DIAL} {@code Intent} containing a {@link Bundle}
* which contains metadata about the call. This {@link Bundle} will be saved into
@@ -369,6 +385,17 @@
public static final String EXTRA_IS_HANDOVER = "android.telecom.extra.IS_HANDOVER";
/**
+ * When {@code true} indicates that a request to create a new connection is for the purpose of
+ * a handover. Note: This is used with the
+ * {@link android.telecom.Call#handoverTo(PhoneAccountHandle, int, Bundle)} API as part of the
+ * internal communication mechanism with the {@link android.telecom.ConnectionService}. It is
+ * not the same as the legacy {@link #EXTRA_IS_HANDOVER} extra.
+ * @hide
+ */
+ public static final String EXTRA_IS_HANDOVER_CONNECTION =
+ "android.telecom.extra.IS_HANDOVER_CONNECTION";
+
+ /**
* Parcelable extra used with {@link #EXTRA_IS_HANDOVER} to indicate the source
* {@link PhoneAccountHandle} when initiating a handover which {@link ConnectionService}
* the handover is from.
@@ -592,12 +619,17 @@
/**
* The boolean indicated by this extra controls whether or not a call is eligible to undergo
* assisted dialing. This extra is stored under {@link #EXTRA_OUTGOING_CALL_EXTRAS}.
- * @hide
*/
public static final String EXTRA_USE_ASSISTED_DIALING =
"android.telecom.extra.USE_ASSISTED_DIALING";
/**
+ * The bundle indicated by this extra store information related to the assisted dialing action.
+ */
+ public static final String EXTRA_ASSISTED_DIALING_TRANSFORMATION_INFO =
+ "android.telecom.extra.ASSISTED_DIALING_TRANSFORMATION_INFO";
+
+ /**
* The following 4 constants define how properties such as phone numbers and names are
* displayed to the user.
*/
@@ -653,7 +685,6 @@
mContext = context;
}
mTelecomServiceOverride = telecomServiceImpl;
- android.telecom.Log.initMd5Sum();
}
/**
@@ -1432,6 +1463,13 @@
public void addNewIncomingCall(PhoneAccountHandle phoneAccount, Bundle extras) {
try {
if (isServiceConnected()) {
+ if (extras != null && extras.getBoolean(EXTRA_IS_HANDOVER) &&
+ mContext.getApplicationContext().getApplicationInfo().targetSdkVersion >
+ Build.VERSION_CODES.O_MR1) {
+ Log.e("TAG", "addNewIncomingCall failed. Use public api " +
+ "acceptHandover for API > O-MR1");
+ // TODO add "return" after DUO team adds support for new handover API
+ }
getTelecomService().addNewIncomingCall(
phoneAccount, extras == null ? new Bundle() : extras);
}
diff --git a/android/telecom/TransformationInfo.java b/android/telecom/TransformationInfo.java
new file mode 100644
index 0000000..3e848c6
--- /dev/null
+++ b/android/telecom/TransformationInfo.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2017 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.telecom;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+/**
+ * A container class to hold information related to the Assisted Dialing operation. All member
+ * variables must be set when constructing a new instance of this class.
+ */
+public final class TransformationInfo implements Parcelable {
+ private String mOriginalNumber;
+ private String mTransformedNumber;
+ private String mUserHomeCountryCode;
+ private String mUserRoamingCountryCode;
+ private int mTransformedNumberCountryCallingCode;
+
+ public TransformationInfo(String originalNumber,
+ String transformedNumber,
+ String userHomeCountryCode,
+ String userRoamingCountryCode,
+ int transformedNumberCountryCallingCode) {
+ String missing = "";
+ if (originalNumber == null) {
+ missing += " mOriginalNumber";
+ }
+ if (transformedNumber == null) {
+ missing += " mTransformedNumber";
+ }
+ if (userHomeCountryCode == null) {
+ missing += " mUserHomeCountryCode";
+ }
+ if (userRoamingCountryCode == null) {
+ missing += " mUserRoamingCountryCode";
+ }
+
+ if (!missing.isEmpty()) {
+ throw new IllegalStateException("Missing required properties:" + missing);
+ }
+ this.mOriginalNumber = originalNumber;
+ this.mTransformedNumber = transformedNumber;
+ this.mUserHomeCountryCode = userHomeCountryCode;
+ this.mUserRoamingCountryCode = userRoamingCountryCode;
+ this.mTransformedNumberCountryCallingCode = transformedNumberCountryCallingCode;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mOriginalNumber);
+ out.writeString(mTransformedNumber);
+ out.writeString(mUserHomeCountryCode);
+ out.writeString(mUserRoamingCountryCode);
+ out.writeInt(mTransformedNumberCountryCallingCode);
+ }
+
+ public static final Parcelable.Creator<TransformationInfo> CREATOR
+ = new Parcelable.Creator<TransformationInfo>() {
+ public TransformationInfo createFromParcel(Parcel in) {
+ return new TransformationInfo(in);
+ }
+
+ public TransformationInfo[] newArray(int size) {
+ return new TransformationInfo[size];
+ }
+ };
+
+ private TransformationInfo(Parcel in) {
+ mOriginalNumber = in.readString();
+ mTransformedNumber = in.readString();
+ mUserHomeCountryCode = in.readString();
+ mUserRoamingCountryCode = in.readString();
+ mTransformedNumberCountryCallingCode = in.readInt();
+ }
+
+ /**
+ * The original number that underwent Assisted Dialing.
+ */
+ public String getOriginalNumber() {
+ return mOriginalNumber;
+ }
+
+ /**
+ * The number after it underwent Assisted Dialing.
+ */
+ public String getTransformedNumber() {
+ return mTransformedNumber;
+ }
+
+ /**
+ * The user's home country code that was used when attempting to transform the number.
+ */
+ public String getUserHomeCountryCode() {
+ return mUserHomeCountryCode;
+ }
+
+ /**
+ * The users's roaming country code that was used when attempting to transform the number.
+ */
+ public String getUserRoamingCountryCode() {
+ return mUserRoamingCountryCode;
+ }
+
+ /**
+ * The country calling code that was used in the transformation.
+ */
+ public int getTransformedNumberCountryCallingCode() {
+ return mTransformedNumberCountryCallingCode;
+ }
+}
diff --git a/android/telephony/RadioNetworkConstants.java b/android/telephony/AccessNetworkConstants.java
similarity index 89%
rename from android/telephony/RadioNetworkConstants.java
rename to android/telephony/AccessNetworkConstants.java
index 5f5dd82..7cd1612 100644
--- a/android/telephony/RadioNetworkConstants.java
+++ b/android/telephony/AccessNetworkConstants.java
@@ -16,24 +16,39 @@
package android.telephony;
-/**
- * Contains radio access network related constants.
- */
-public final class RadioNetworkConstants {
+import android.annotation.SystemApi;
- public static final class RadioAccessNetworks {
+/**
+ * Contains access network related constants.
+ */
+public final class AccessNetworkConstants {
+
+ public static final class AccessNetworkType {
+ public static final int UNKNOWN = 0;
public static final int GERAN = 1;
public static final int UTRAN = 2;
public static final int EUTRAN = 3;
- /** @hide */
public static final int CDMA2000 = 4;
+ public static final int IWLAN = 5;
+ }
+
+ /**
+ * Wireless transportation type
+ * @hide
+ */
+ @SystemApi
+ public static final class TransportType {
+ /** Wireless Wide Area Networks (i.e. Cellular) */
+ public static final int WWAN = 1;
+ /** Wireless Local Area Networks (i.e. Wifi) */
+ public static final int WLAN = 2;
}
/**
* Frenquency bands for GERAN.
* http://www.etsi.org/deliver/etsi_ts/145000_145099/145005/14.00.00_60/ts_145005v140000p.pdf
*/
- public static final class GeranBands {
+ public static final class GeranBand {
public static final int BAND_T380 = 1;
public static final int BAND_T410 = 2;
public static final int BAND_450 = 3;
@@ -54,7 +69,7 @@
* Frenquency bands for UTRAN.
* http://www.etsi.org/deliver/etsi_ts/125100_125199/125104/13.03.00_60/ts_125104v130p.pdf
*/
- public static final class UtranBands {
+ public static final class UtranBand {
public static final int BAND_1 = 1;
public static final int BAND_2 = 2;
public static final int BAND_3 = 3;
@@ -83,7 +98,7 @@
* Frenquency bands for EUTRAN.
* http://www.etsi.org/deliver/etsi_ts/136100_136199/136101/14.03.00_60/ts_136101v140p.pdf
*/
- public static final class EutranBands {
+ public static final class EutranBand {
public static final int BAND_1 = 1;
public static final int BAND_2 = 2;
public static final int BAND_3 = 3;
diff --git a/android/telephony/CarrierConfigManager.java b/android/telephony/CarrierConfigManager.java
index 6a47d05..91d86c6 100644
--- a/android/telephony/CarrierConfigManager.java
+++ b/android/telephony/CarrierConfigManager.java
@@ -39,13 +39,29 @@
private final static String TAG = "CarrierConfigManager";
/**
+ * Extra included in {@link #ACTION_CARRIER_CONFIG_CHANGED} to indicate the slot index that the
+ * broadcast is for.
+ */
+ public static final String EXTRA_SLOT_INDEX = "android.telephony.extra.SLOT_INDEX";
+
+ /**
+ * Optional extra included in {@link #ACTION_CARRIER_CONFIG_CHANGED} to indicate the
+ * subscription index that the broadcast is for, if a valid one is available.
+ */
+ public static final String EXTRA_SUBSCRIPTION_INDEX =
+ SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX;
+
+ /**
* @hide
*/
public CarrierConfigManager() {
}
/**
- * This intent is broadcast by the system when carrier config changes.
+ * This intent is broadcast by the system when carrier config changes. An int is specified in
+ * {@link #EXTRA_SLOT_INDEX} to indicate the slot index that this is for. An optional int extra
+ * {@link #EXTRA_SUBSCRIPTION_INDEX} is included to indicate the subscription index if a valid
+ * one is available for the slot index.
*/
public static final String
ACTION_CARRIER_CONFIG_CHANGED = "android.telephony.action.CARRIER_CONFIG_CHANGED";
@@ -275,7 +291,6 @@
*
* @see SubscriptionManager#getSubscriptionPlans(int)
* @see SubscriptionManager#setSubscriptionPlans(int, java.util.List)
- * @hide
*/
@SystemApi
public static final String KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING =
@@ -337,6 +352,19 @@
"notify_handover_video_from_wifi_to_lte_bool";
/**
+ * Flag specifying whether the carrier wants to notify the user when a VT call has been handed
+ * over from LTE to WIFI.
+ * <p>
+ * The handover notification is sent as a
+ * {@link TelephonyManager#EVENT_HANDOVER_VIDEO_FROM_LTE_TO_WIFI}
+ * {@link android.telecom.Connection} event, which an {@link android.telecom.InCallService}
+ * should use to trigger the display of a user-facing message.
+ * @hide
+ */
+ public static final String KEY_NOTIFY_HANDOVER_VIDEO_FROM_LTE_TO_WIFI_BOOL =
+ "notify_handover_video_from_lte_to_wifi_bool";
+
+ /**
* Flag specifying whether the carrier supports downgrading a video call (tx, rx or tx/rx)
* directly to an audio call.
* @hide
@@ -947,8 +975,9 @@
public static final String KEY_CARRIER_NAME_OVERRIDE_BOOL = "carrier_name_override_bool";
/**
- * String to identify carrier name in CarrierConfig app. This string is used only if
- * #KEY_CARRIER_NAME_OVERRIDE_BOOL is true
+ * String to identify carrier name in CarrierConfig app. This string overrides SPN if
+ * #KEY_CARRIER_NAME_OVERRIDE_BOOL is true; otherwise, it will be used if its value is provided
+ * and SPN is unavailable
* @hide
*/
public static final String KEY_CARRIER_NAME_STRING = "carrier_name_string";
@@ -1003,6 +1032,13 @@
public static final String KEY_ALWAYS_SHOW_DATA_RAT_ICON_BOOL =
"always_show_data_rat_icon_bool";
+ /**
+ * Boolean to decide whether to show precise call failed cause to user
+ * @hide
+ */
+ public static final String KEY_SHOW_PRECISE_FAILED_CAUSE_BOOL =
+ "show_precise_failed_cause_bool";
+
// These variables are used by the MMS service and exposed through another API, {@link
// SmsManager}. The variable names and string values are copied from there.
public static final String KEY_MMS_ALIAS_ENABLED_BOOL = "aliasEnabled";
@@ -1623,6 +1659,13 @@
"roaming_operator_string_array";
/**
+ * Controls whether Assisted Dialing is enabled and the preference is shown. This feature
+ * transforms numbers when the user is roaming.
+ */
+ public static final String KEY_ASSISTED_DIALING_ENABLED_BOOL =
+ "assisted_dialing_enabled_bool";
+
+ /**
* URL from which the proto containing the public key of the Carrier used for
* IMSI encryption will be downloaded.
* @hide
@@ -1724,6 +1767,22 @@
*/
public static final String KEY_CARRIER_CONFIG_APPLIED_BOOL = "carrier_config_applied_bool";
+ /**
+ * Determines whether we should show a warning asking the user to check with their carrier
+ * on pricing when the user enabled data roaming.
+ * default to false.
+ * @hide
+ */
+ public static final String KEY_CHECK_PRICING_WITH_CARRIER_FOR_DATA_ROAMING_BOOL =
+ "check_pricing_with_carrier_data_roaming_bool";
+
+ /**
+ * List of thresholds of RSRP for determining the display level of LTE signal bar.
+ * @hide
+ */
+ public static final String KEY_LTE_RSRP_THRESHOLDS_INT_ARRAY =
+ "lte_rsrp_thresholds_int_array";
+
/** The default value for every variable. */
private final static PersistableBundle sDefaults;
@@ -1740,6 +1799,7 @@
sDefaults.putBoolean(KEY_CARRIER_VOLTE_AVAILABLE_BOOL, false);
sDefaults.putBoolean(KEY_CARRIER_VT_AVAILABLE_BOOL, false);
sDefaults.putBoolean(KEY_NOTIFY_HANDOVER_VIDEO_FROM_WIFI_TO_LTE_BOOL, false);
+ sDefaults.putBoolean(KEY_NOTIFY_HANDOVER_VIDEO_FROM_LTE_TO_WIFI_BOOL, false);
sDefaults.putBoolean(KEY_SUPPORT_DOWNGRADE_VT_TO_AUDIO_BOOL, true);
sDefaults.putString(KEY_DEFAULT_VM_NUMBER_STRING, "");
sDefaults.putBoolean(KEY_CONFIG_TELEPHONY_USE_OWN_NUMBER_FOR_VOICEMAIL_BOOL, false);
@@ -2002,14 +2062,26 @@
false);
sDefaults.putStringArray(KEY_NON_ROAMING_OPERATOR_STRING_ARRAY, null);
sDefaults.putStringArray(KEY_ROAMING_OPERATOR_STRING_ARRAY, null);
+ sDefaults.putBoolean(KEY_ASSISTED_DIALING_ENABLED_BOOL, true);
sDefaults.putBoolean(KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, false);
sDefaults.putBoolean(KEY_RTT_SUPPORTED_BOOL, false);
sDefaults.putBoolean(KEY_DISABLE_CHARGE_INDICATION_BOOL, false);
sDefaults.putStringArray(KEY_FEATURE_ACCESS_CODES_STRING_ARRAY, null);
sDefaults.putBoolean(KEY_IDENTIFY_HIGH_DEFINITION_CALLS_IN_CALL_LOG_BOOL, false);
+ sDefaults.putBoolean(KEY_SHOW_PRECISE_FAILED_CAUSE_BOOL, false);
sDefaults.putBoolean(KEY_SPN_DISPLAY_RULE_USE_ROAMING_FROM_SERVICE_STATE_BOOL, false);
sDefaults.putBoolean(KEY_ALWAYS_SHOW_DATA_RAT_ICON_BOOL, false);
sDefaults.putBoolean(KEY_CARRIER_CONFIG_APPLIED_BOOL, false);
+ sDefaults.putBoolean(KEY_CHECK_PRICING_WITH_CARRIER_FOR_DATA_ROAMING_BOOL, false);
+ sDefaults.putIntArray(KEY_LTE_RSRP_THRESHOLDS_INT_ARRAY,
+ new int[] {
+ -140, /* SIGNAL_STRENGTH_NONE_OR_UNKNOWN */
+ -128, /* SIGNAL_STRENGTH_POOR */
+ -118, /* SIGNAL_STRENGTH_MODERATE */
+ -108, /* SIGNAL_STRENGTH_GOOD */
+ -98, /* SIGNAL_STRENGTH_GREAT */
+ -44
+ });
}
/**
diff --git a/android/telephony/CellIdentity.java b/android/telephony/CellIdentity.java
new file mode 100644
index 0000000..e092d52
--- /dev/null
+++ b/android/telephony/CellIdentity.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2017 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.telephony;
+
+import android.annotation.CallSuper;
+import android.annotation.IntDef;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * CellIdentity represents the identity of a unique cell. This is the base class for
+ * CellIdentityXxx which represents cell identity for specific network access technology.
+ */
+public abstract class CellIdentity implements Parcelable {
+ /**
+ * Cell identity type
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "TYPE_", value = {TYPE_GSM, TYPE_CDMA, TYPE_LTE, TYPE_WCDMA, TYPE_TDSCDMA})
+ public @interface Type {}
+
+ /**
+ * Unknown cell identity type
+ * @hide
+ */
+ public static final int TYPE_UNKNOWN = 0;
+ /**
+ * GSM cell identity type
+ * @hide
+ */
+ public static final int TYPE_GSM = 1;
+ /**
+ * CDMA cell identity type
+ * @hide
+ */
+ public static final int TYPE_CDMA = 2;
+ /**
+ * LTE cell identity type
+ * @hide
+ */
+ public static final int TYPE_LTE = 3;
+ /**
+ * WCDMA cell identity type
+ * @hide
+ */
+ public static final int TYPE_WCDMA = 4;
+ /**
+ * TDS-CDMA cell identity type
+ * @hide
+ */
+ public static final int TYPE_TDSCDMA = 5;
+
+ // Log tag
+ /** @hide */
+ protected final String mTag;
+ // Cell identity type
+ /** @hide */
+ protected final int mType;
+ // 3-digit Mobile Country Code in string format. Null for CDMA cell identity.
+ /** @hide */
+ protected final String mMccStr;
+ // 2 or 3-digit Mobile Network Code in string format. Null for CDMA cell identity.
+ /** @hide */
+ protected final String mMncStr;
+
+ /** @hide */
+ protected CellIdentity(String tag, int type, String mcc, String mnc) {
+ mTag = tag;
+ mType = type;
+
+ // Only allow INT_MAX if unknown string mcc/mnc
+ if (mcc == null || mcc.matches("^[0-9]{3}$")) {
+ mMccStr = mcc;
+ } else if (mcc.isEmpty() || mcc.equals(String.valueOf(Integer.MAX_VALUE))) {
+ // If the mccStr is empty or unknown, set it as null.
+ mMccStr = null;
+ } else {
+ // TODO: b/69384059 Should throw IllegalArgumentException for the invalid MCC format
+ // after the bug got fixed.
+ mMccStr = null;
+ log("invalid MCC format: " + mcc);
+ }
+
+ if (mnc == null || mnc.matches("^[0-9]{2,3}$")) {
+ mMncStr = mnc;
+ } else if (mnc.isEmpty() || mnc.equals(String.valueOf(Integer.MAX_VALUE))) {
+ // If the mncStr is empty or unknown, set it as null.
+ mMncStr = null;
+ } else {
+ // TODO: b/69384059 Should throw IllegalArgumentException for the invalid MNC format
+ // after the bug got fixed.
+ mMncStr = null;
+ log("invalid MNC format: " + mnc);
+ }
+ }
+
+ /** Implement the Parcelable interface */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * @hide
+ * @return The type of the cell identity
+ */
+ public @Type int getType() { return mType; }
+
+ /**
+ * Used by child classes for parceling.
+ *
+ * @hide
+ */
+ @CallSuper
+ public void writeToParcel(Parcel dest, int type) {
+ dest.writeInt(type);
+ dest.writeString(mMccStr);
+ dest.writeString(mMncStr);
+ }
+
+ /**
+ * Construct from Parcel
+ * @hide
+ */
+ protected CellIdentity(String tag, int type, Parcel source) {
+ this(tag, type, source.readString(), source.readString());
+ }
+
+ /** Implement the Parcelable interface */
+ public static final Creator<CellIdentity> CREATOR =
+ new Creator<CellIdentity>() {
+ @Override
+ public CellIdentity createFromParcel(Parcel in) {
+ int type = in.readInt();
+ switch (type) {
+ case TYPE_GSM: return CellIdentityGsm.createFromParcelBody(in);
+ case TYPE_WCDMA: return CellIdentityWcdma.createFromParcelBody(in);
+ case TYPE_CDMA: return CellIdentityCdma.createFromParcelBody(in);
+ case TYPE_LTE: return CellIdentityLte.createFromParcelBody(in);
+ case TYPE_TDSCDMA: return CellIdentityTdscdma.createFromParcelBody(in);
+ default: throw new IllegalArgumentException("Bad Cell identity Parcel");
+ }
+ }
+
+ @Override
+ public CellIdentity[] newArray(int size) {
+ return new CellIdentity[size];
+ }
+ };
+
+ /** @hide */
+ protected void log(String s) {
+ Rlog.w(mTag, s);
+ }
+}
\ No newline at end of file
diff --git a/android/telephony/CellIdentityCdma.java b/android/telephony/CellIdentityCdma.java
index ddc938e..2e1d1dc 100644
--- a/android/telephony/CellIdentityCdma.java
+++ b/android/telephony/CellIdentityCdma.java
@@ -17,8 +17,6 @@
package android.telephony;
import android.os.Parcel;
-import android.os.Parcelable;
-import android.telephony.Rlog;
import android.text.TextUtils;
import java.util.Objects;
@@ -26,9 +24,8 @@
/**
* CellIdentity is to represent a unique CDMA cell
*/
-public final class CellIdentityCdma implements Parcelable {
-
- private static final String LOG_TAG = "CellSignalStrengthCdma";
+public final class CellIdentityCdma extends CellIdentity {
+ private static final String TAG = CellIdentityCdma.class.getSimpleName();
private static final boolean DBG = false;
// Network Id 0..65535
@@ -60,6 +57,7 @@
* @hide
*/
public CellIdentityCdma() {
+ super(TAG, TYPE_CDMA, null, null);
mNetworkId = Integer.MAX_VALUE;
mSystemId = Integer.MAX_VALUE;
mBasestationId = Integer.MAX_VALUE;
@@ -81,7 +79,7 @@
*
* @hide
*/
- public CellIdentityCdma (int nid, int sid, int bid, int lon, int lat) {
+ public CellIdentityCdma(int nid, int sid, int bid, int lon, int lat) {
this(nid, sid, bid, lon, lat, null, null);
}
@@ -99,8 +97,9 @@
*
* @hide
*/
- public CellIdentityCdma (int nid, int sid, int bid, int lon, int lat, String alphal,
+ public CellIdentityCdma(int nid, int sid, int bid, int lon, int lat, String alphal,
String alphas) {
+ super(TAG, TYPE_CDMA, null, null);
mNetworkId = nid;
mSystemId = sid;
mBasestationId = bid;
@@ -196,40 +195,33 @@
CellIdentityCdma o = (CellIdentityCdma) other;
- return mNetworkId == o.mNetworkId &&
- mSystemId == o.mSystemId &&
- mBasestationId == o.mBasestationId &&
- mLatitude == o.mLatitude &&
- mLongitude == o.mLongitude &&
- TextUtils.equals(mAlphaLong, o.mAlphaLong) &&
- TextUtils.equals(mAlphaShort, o.mAlphaShort);
+ return mNetworkId == o.mNetworkId
+ && mSystemId == o.mSystemId
+ && mBasestationId == o.mBasestationId
+ && mLatitude == o.mLatitude
+ && mLongitude == o.mLongitude
+ && TextUtils.equals(mAlphaLong, o.mAlphaLong)
+ && TextUtils.equals(mAlphaShort, o.mAlphaShort);
}
@Override
public String toString() {
- StringBuilder sb = new StringBuilder("CellIdentityCdma:{");
- sb.append(" mNetworkId="); sb.append(mNetworkId);
- sb.append(" mSystemId="); sb.append(mSystemId);
- sb.append(" mBasestationId="); sb.append(mBasestationId);
- sb.append(" mLongitude="); sb.append(mLongitude);
- sb.append(" mLatitude="); sb.append(mLatitude);
- sb.append(" mAlphaLong="); sb.append(mAlphaLong);
- sb.append(" mAlphaShort="); sb.append(mAlphaShort);
- sb.append("}");
-
- return sb.toString();
- }
-
- /** Implement the Parcelable interface */
- @Override
- public int describeContents() {
- return 0;
+ return new StringBuilder(TAG)
+ .append(":{ mNetworkId=").append(mNetworkId)
+ .append(" mSystemId=").append(mSystemId)
+ .append(" mBasestationId=").append(mBasestationId)
+ .append(" mLongitude=").append(mLongitude)
+ .append(" mLatitude=").append(mLatitude)
+ .append(" mAlphaLong=").append(mAlphaLong)
+ .append(" mAlphaShort=").append(mAlphaShort)
+ .append("}").toString();
}
/** Implement the Parcelable interface */
@Override
public void writeToParcel(Parcel dest, int flags) {
if (DBG) log("writeToParcel(Parcel, int): " + toString());
+ super.writeToParcel(dest, TYPE_CDMA);
dest.writeInt(mNetworkId);
dest.writeInt(mSystemId);
dest.writeInt(mBasestationId);
@@ -241,10 +233,16 @@
/** Construct from Parcel, type has already been processed */
private CellIdentityCdma(Parcel in) {
- this(in.readInt(), in.readInt(), in.readInt(), in.readInt(), in.readInt(),
- in.readString(), in.readString());
+ super(TAG, TYPE_CDMA, in);
+ mNetworkId = in.readInt();
+ mSystemId = in.readInt();
+ mBasestationId = in.readInt();
+ mLongitude = in.readInt();
+ mLatitude = in.readInt();
+ mAlphaLong = in.readString();
+ mAlphaShort = in.readString();
- if (DBG) log("CellIdentityCdma(Parcel): " + toString());
+ if (DBG) log(toString());
}
/** Implement the Parcelable interface */
@@ -253,7 +251,8 @@
new Creator<CellIdentityCdma>() {
@Override
public CellIdentityCdma createFromParcel(Parcel in) {
- return new CellIdentityCdma(in);
+ in.readInt(); // skip
+ return createFromParcelBody(in);
}
@Override
@@ -262,10 +261,8 @@
}
};
- /**
- * log
- */
- private static void log(String s) {
- Rlog.w(LOG_TAG, s);
+ /** @hide */
+ protected static CellIdentityCdma createFromParcelBody(Parcel in) {
+ return new CellIdentityCdma(in);
}
}
diff --git a/android/telephony/CellIdentityGsm.java b/android/telephony/CellIdentityGsm.java
index 376e6aa..f948f81 100644
--- a/android/telephony/CellIdentityGsm.java
+++ b/android/telephony/CellIdentityGsm.java
@@ -17,8 +17,6 @@
package android.telephony;
import android.os.Parcel;
-import android.os.Parcelable;
-import android.telephony.Rlog;
import android.text.TextUtils;
import java.util.Objects;
@@ -26,9 +24,8 @@
/**
* CellIdentity to represent a unique GSM cell
*/
-public final class CellIdentityGsm implements Parcelable {
-
- private static final String LOG_TAG = "CellIdentityGsm";
+public final class CellIdentityGsm extends CellIdentity {
+ private static final String TAG = CellIdentityGsm.class.getSimpleName();
private static final boolean DBG = false;
// 16-bit Location Area Code, 0..65535
@@ -39,10 +36,6 @@
private final int mArfcn;
// 6-bit Base Station Identity Code
private final int mBsic;
- // 3-digit Mobile Country Code in string format
- private final String mMccStr;
- // 2 or 3-digit Mobile Network Code in string format
- private final String mMncStr;
// long alpha Operator Name String or Enhanced Operator Name String
private final String mAlphaLong;
// short alpha Operator Name String or Enhanced Operator Name String
@@ -52,12 +45,11 @@
* @hide
*/
public CellIdentityGsm() {
+ super(TAG, TYPE_GSM, null, null);
mLac = Integer.MAX_VALUE;
mCid = Integer.MAX_VALUE;
mArfcn = Integer.MAX_VALUE;
mBsic = Integer.MAX_VALUE;
- mMccStr = null;
- mMncStr = null;
mAlphaLong = null;
mAlphaShort = null;
}
@@ -70,7 +62,7 @@
*
* @hide
*/
- public CellIdentityGsm (int mcc, int mnc, int lac, int cid) {
+ public CellIdentityGsm(int mcc, int mnc, int lac, int cid) {
this(lac, cid, Integer.MAX_VALUE, Integer.MAX_VALUE,
String.valueOf(mcc), String.valueOf(mnc), null, null);
}
@@ -86,7 +78,7 @@
*
* @hide
*/
- public CellIdentityGsm (int mcc, int mnc, int lac, int cid, int arfcn, int bsic) {
+ public CellIdentityGsm(int mcc, int mnc, int lac, int cid, int arfcn, int bsic) {
this(lac, cid, arfcn, bsic, String.valueOf(mcc), String.valueOf(mnc), null, null);
}
@@ -103,8 +95,9 @@
*
* @hide
*/
- public CellIdentityGsm (int lac, int cid, int arfcn, int bsic, String mccStr,
+ public CellIdentityGsm(int lac, int cid, int arfcn, int bsic, String mccStr,
String mncStr, String alphal, String alphas) {
+ super(TAG, TYPE_GSM, mccStr, mncStr);
mLac = lac;
mCid = cid;
mArfcn = arfcn;
@@ -112,31 +105,6 @@
// for inbound parcels
mBsic = (bsic == 0xFF) ? Integer.MAX_VALUE : bsic;
- // Only allow INT_MAX if unknown string mcc/mnc
- if (mccStr == null || mccStr.matches("^[0-9]{3}$")) {
- mMccStr = mccStr;
- } else if (mccStr.isEmpty() || mccStr.equals(String.valueOf(Integer.MAX_VALUE))) {
- // If the mccStr is empty or unknown, set it as null.
- mMccStr = null;
- } else {
- // TODO: b/69384059 Should throw IllegalArgumentException for the invalid MCC format
- // after the bug got fixed.
- mMccStr = null;
- log("invalid MCC format: " + mccStr);
- }
-
- if (mncStr == null || mncStr.matches("^[0-9]{2,3}$")) {
- mMncStr = mncStr;
- } else if (mncStr.isEmpty() || mncStr.equals(String.valueOf(Integer.MAX_VALUE))) {
- // If the mncStr is empty or unknown, set it as null.
- mMncStr = null;
- } else {
- // TODO: b/69384059 Should throw IllegalArgumentException for the invalid MNC format
- // after the bug got fixed.
- mMncStr = null;
- log("invalid MNC format: " + mncStr);
- }
-
mAlphaLong = alphal;
mAlphaShort = alphas;
}
@@ -237,6 +205,7 @@
/**
+ * @deprecated Primary Scrambling Code is not applicable to GSM.
* @return Integer.MAX_VALUE, undefined for GSM
*/
@Deprecated
@@ -260,58 +229,54 @@
}
CellIdentityGsm o = (CellIdentityGsm) other;
- return mLac == o.mLac &&
- mCid == o.mCid &&
- mArfcn == o.mArfcn &&
- mBsic == o.mBsic &&
- TextUtils.equals(mMccStr, o.mMccStr) &&
- TextUtils.equals(mMncStr, o.mMncStr) &&
- TextUtils.equals(mAlphaLong, o.mAlphaLong) &&
- TextUtils.equals(mAlphaShort, o.mAlphaShort);
+ return mLac == o.mLac
+ && mCid == o.mCid
+ && mArfcn == o.mArfcn
+ && mBsic == o.mBsic
+ && TextUtils.equals(mMccStr, o.mMccStr)
+ && TextUtils.equals(mMncStr, o.mMncStr)
+ && TextUtils.equals(mAlphaLong, o.mAlphaLong)
+ && TextUtils.equals(mAlphaShort, o.mAlphaShort);
}
@Override
public String toString() {
- StringBuilder sb = new StringBuilder("CellIdentityGsm:{");
- sb.append(" mLac=").append(mLac);
- sb.append(" mCid=").append(mCid);
- sb.append(" mArfcn=").append(mArfcn);
- sb.append(" mBsic=").append("0x").append(Integer.toHexString(mBsic));
- sb.append(" mMcc=").append(mMccStr);
- sb.append(" mMnc=").append(mMncStr);
- sb.append(" mAlphaLong=").append(mAlphaLong);
- sb.append(" mAlphaShort=").append(mAlphaShort);
- sb.append("}");
-
- return sb.toString();
- }
-
- /** Implement the Parcelable interface */
- @Override
- public int describeContents() {
- return 0;
+ return new StringBuilder(TAG)
+ .append(":{ mLac=").append(mLac)
+ .append(" mCid=").append(mCid)
+ .append(" mArfcn=").append(mArfcn)
+ .append(" mBsic=").append("0x").append(Integer.toHexString(mBsic))
+ .append(" mMcc=").append(mMccStr)
+ .append(" mMnc=").append(mMncStr)
+ .append(" mAlphaLong=").append(mAlphaLong)
+ .append(" mAlphaShort=").append(mAlphaShort)
+ .append("}").toString();
}
/** Implement the Parcelable interface */
@Override
public void writeToParcel(Parcel dest, int flags) {
if (DBG) log("writeToParcel(Parcel, int): " + toString());
+ super.writeToParcel(dest, TYPE_GSM);
dest.writeInt(mLac);
dest.writeInt(mCid);
dest.writeInt(mArfcn);
dest.writeInt(mBsic);
- dest.writeString(mMccStr);
- dest.writeString(mMncStr);
dest.writeString(mAlphaLong);
dest.writeString(mAlphaShort);
}
/** Construct from Parcel, type has already been processed */
private CellIdentityGsm(Parcel in) {
- this(in.readInt(), in.readInt(), in.readInt(), in.readInt(), in.readString(),
- in.readString(), in.readString(), in.readString());
+ super(TAG, TYPE_GSM, in);
+ mLac = in.readInt();
+ mCid = in.readInt();
+ mArfcn = in.readInt();
+ mBsic = in.readInt();
+ mAlphaLong = in.readString();
+ mAlphaShort = in.readString();
- if (DBG) log("CellIdentityGsm(Parcel): " + toString());
+ if (DBG) log(toString());
}
/** Implement the Parcelable interface */
@@ -320,7 +285,8 @@
new Creator<CellIdentityGsm>() {
@Override
public CellIdentityGsm createFromParcel(Parcel in) {
- return new CellIdentityGsm(in);
+ in.readInt(); // skip
+ return createFromParcelBody(in);
}
@Override
@@ -329,10 +295,8 @@
}
};
- /**
- * log
- */
- private static void log(String s) {
- Rlog.w(LOG_TAG, s);
+ /** @hide */
+ protected static CellIdentityGsm createFromParcelBody(Parcel in) {
+ return new CellIdentityGsm(in);
}
-}
\ No newline at end of file
+}
diff --git a/android/telephony/CellIdentityLte.java b/android/telephony/CellIdentityLte.java
index 6ca5daf..7f20c8a 100644
--- a/android/telephony/CellIdentityLte.java
+++ b/android/telephony/CellIdentityLte.java
@@ -17,8 +17,6 @@
package android.telephony;
import android.os.Parcel;
-import android.os.Parcelable;
-import android.telephony.Rlog;
import android.text.TextUtils;
import java.util.Objects;
@@ -26,9 +24,8 @@
/**
* CellIdentity is to represent a unique LTE cell
*/
-public final class CellIdentityLte implements Parcelable {
-
- private static final String LOG_TAG = "CellIdentityLte";
+public final class CellIdentityLte extends CellIdentity {
+ private static final String TAG = CellIdentityLte.class.getSimpleName();
private static final boolean DBG = false;
// 28-bit cell identity
@@ -39,10 +36,6 @@
private final int mTac;
// 18-bit Absolute RF Channel Number
private final int mEarfcn;
- // 3-digit Mobile Country Code in string format
- private final String mMccStr;
- // 2 or 3-digit Mobile Network Code in string format
- private final String mMncStr;
// long alpha Operator Name String or Enhanced Operator Name String
private final String mAlphaLong;
// short alpha Operator Name String or Enhanced Operator Name String
@@ -52,12 +45,11 @@
* @hide
*/
public CellIdentityLte() {
+ super(TAG, TYPE_LTE, null, null);
mCi = Integer.MAX_VALUE;
mPci = Integer.MAX_VALUE;
mTac = Integer.MAX_VALUE;
mEarfcn = Integer.MAX_VALUE;
- mMccStr = null;
- mMncStr = null;
mAlphaLong = null;
mAlphaShort = null;
}
@@ -72,7 +64,7 @@
*
* @hide
*/
- public CellIdentityLte (int mcc, int mnc, int ci, int pci, int tac) {
+ public CellIdentityLte(int mcc, int mnc, int ci, int pci, int tac) {
this(ci, pci, tac, Integer.MAX_VALUE, String.valueOf(mcc), String.valueOf(mnc), null, null);
}
@@ -87,7 +79,7 @@
*
* @hide
*/
- public CellIdentityLte (int mcc, int mnc, int ci, int pci, int tac, int earfcn) {
+ public CellIdentityLte(int mcc, int mnc, int ci, int pci, int tac, int earfcn) {
this(ci, pci, tac, earfcn, String.valueOf(mcc), String.valueOf(mnc), null, null);
}
@@ -104,38 +96,13 @@
*
* @hide
*/
- public CellIdentityLte (int ci, int pci, int tac, int earfcn, String mccStr,
+ public CellIdentityLte(int ci, int pci, int tac, int earfcn, String mccStr,
String mncStr, String alphal, String alphas) {
+ super(TAG, TYPE_LTE, mccStr, mncStr);
mCi = ci;
mPci = pci;
mTac = tac;
mEarfcn = earfcn;
-
- // Only allow INT_MAX if unknown string mcc/mnc
- if (mccStr == null || mccStr.matches("^[0-9]{3}$")) {
- mMccStr = mccStr;
- } else if (mccStr.isEmpty() || mccStr.equals(String.valueOf(Integer.MAX_VALUE))) {
- // If the mccStr is empty or unknown, set it as null.
- mMccStr = null;
- } else {
- // TODO: b/69384059 Should throw IllegalArgumentException for the invalid MCC format
- // after the bug got fixed.
- mMccStr = null;
- log("invalid MCC format: " + mccStr);
- }
-
- if (mncStr == null || mncStr.matches("^[0-9]{2,3}$")) {
- mMncStr = mncStr;
- } else if (mncStr.isEmpty() || mncStr.equals(String.valueOf(Integer.MAX_VALUE))) {
- // If the mncStr is empty or unknown, set it as null.
- mMncStr = null;
- } else {
- // TODO: b/69384059 Should throw IllegalArgumentException for the invalid MNC format
- // after the bug got fixed.
- mMncStr = null;
- log("invalid MNC format: " + mncStr);
- }
-
mAlphaLong = alphal;
mAlphaShort = alphas;
}
@@ -248,58 +215,54 @@
}
CellIdentityLte o = (CellIdentityLte) other;
- return mCi == o.mCi &&
- mPci == o.mPci &&
- mTac == o.mTac &&
- mEarfcn == o.mEarfcn &&
- TextUtils.equals(mMccStr, o.mMccStr) &&
- TextUtils.equals(mMncStr, o.mMncStr) &&
- TextUtils.equals(mAlphaLong, o.mAlphaLong) &&
- TextUtils.equals(mAlphaShort, o.mAlphaShort);
+ return mCi == o.mCi
+ && mPci == o.mPci
+ && mTac == o.mTac
+ && mEarfcn == o.mEarfcn
+ && TextUtils.equals(mMccStr, o.mMccStr)
+ && TextUtils.equals(mMncStr, o.mMncStr)
+ && TextUtils.equals(mAlphaLong, o.mAlphaLong)
+ && TextUtils.equals(mAlphaShort, o.mAlphaShort);
}
@Override
public String toString() {
- StringBuilder sb = new StringBuilder("CellIdentityLte:{");
- sb.append(" mCi="); sb.append(mCi);
- sb.append(" mPci="); sb.append(mPci);
- sb.append(" mTac="); sb.append(mTac);
- sb.append(" mEarfcn="); sb.append(mEarfcn);
- sb.append(" mMcc="); sb.append(mMccStr);
- sb.append(" mMnc="); sb.append(mMncStr);
- sb.append(" mAlphaLong="); sb.append(mAlphaLong);
- sb.append(" mAlphaShort="); sb.append(mAlphaShort);
- sb.append("}");
-
- return sb.toString();
- }
-
- /** Implement the Parcelable interface */
- @Override
- public int describeContents() {
- return 0;
+ return new StringBuilder(TAG)
+ .append(":{ mCi=").append(mCi)
+ .append(" mPci=").append(mPci)
+ .append(" mTac=").append(mTac)
+ .append(" mEarfcn=").append(mEarfcn)
+ .append(" mMcc=").append(mMccStr)
+ .append(" mMnc=").append(mMncStr)
+ .append(" mAlphaLong=").append(mAlphaLong)
+ .append(" mAlphaShort=").append(mAlphaShort)
+ .append("}").toString();
}
/** Implement the Parcelable interface */
@Override
public void writeToParcel(Parcel dest, int flags) {
if (DBG) log("writeToParcel(Parcel, int): " + toString());
+ super.writeToParcel(dest, TYPE_LTE);
dest.writeInt(mCi);
dest.writeInt(mPci);
dest.writeInt(mTac);
dest.writeInt(mEarfcn);
- dest.writeString(mMccStr);
- dest.writeString(mMncStr);
dest.writeString(mAlphaLong);
dest.writeString(mAlphaShort);
}
/** Construct from Parcel, type has already been processed */
private CellIdentityLte(Parcel in) {
- this(in.readInt(), in.readInt(), in.readInt(), in.readInt(), in.readString(),
- in.readString(), in.readString(), in.readString());
+ super(TAG, TYPE_LTE, in);
+ mCi = in.readInt();
+ mPci = in.readInt();
+ mTac = in.readInt();
+ mEarfcn = in.readInt();
+ mAlphaLong = in.readString();
+ mAlphaShort = in.readString();
- if (DBG) log("CellIdentityLte(Parcel): " + toString());
+ if (DBG) log(toString());
}
/** Implement the Parcelable interface */
@@ -308,7 +271,8 @@
new Creator<CellIdentityLte>() {
@Override
public CellIdentityLte createFromParcel(Parcel in) {
- return new CellIdentityLte(in);
+ in.readInt(); // skip;
+ return createFromParcelBody(in);
}
@Override
@@ -317,10 +281,8 @@
}
};
- /**
- * log
- */
- private static void log(String s) {
- Rlog.w(LOG_TAG, s);
+ /** @hide */
+ protected static CellIdentityLte createFromParcelBody(Parcel in) {
+ return new CellIdentityLte(in);
}
-}
\ No newline at end of file
+}
diff --git a/android/telephony/CellIdentityTdscdma.java b/android/telephony/CellIdentityTdscdma.java
new file mode 100644
index 0000000..001d19f
--- /dev/null
+++ b/android/telephony/CellIdentityTdscdma.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2017 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.telephony;
+
+import android.os.Parcel;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * CellIdentity is to represent a unique TD-SCDMA cell
+ */
+public final class CellIdentityTdscdma extends CellIdentity {
+ private static final String TAG = CellIdentityTdscdma.class.getSimpleName();
+ private static final boolean DBG = false;
+
+ // 16-bit Location Area Code, 0..65535, INT_MAX if unknown.
+ private final int mLac;
+ // 28-bit UMTS Cell Identity described in TS 25.331, 0..268435455, INT_MAX if unknown.
+ private final int mCid;
+ // 8-bit Cell Parameters ID described in TS 25.331, 0..127, INT_MAX if unknown.
+ private final int mCpid;
+
+ /**
+ * @hide
+ */
+ public CellIdentityTdscdma() {
+ super(TAG, TYPE_TDSCDMA, null, null);
+ mLac = Integer.MAX_VALUE;
+ mCid = Integer.MAX_VALUE;
+ mCpid = Integer.MAX_VALUE;
+ }
+
+ /**
+ * @param mcc 3-digit Mobile Country Code, 0..999
+ * @param mnc 2 or 3-digit Mobile Network Code, 0..999
+ * @param lac 16-bit Location Area Code, 0..65535, INT_MAX if unknown
+ * @param cid 28-bit UMTS Cell Identity described in TS 25.331, 0..268435455, INT_MAX if unknown
+ * @param cpid 8-bit Cell Parameters ID described in TS 25.331, 0..127, INT_MAX if unknown
+ *
+ * @hide
+ */
+ public CellIdentityTdscdma(int mcc, int mnc, int lac, int cid, int cpid) {
+ this(String.valueOf(mcc), String.valueOf(mnc), lac, cid, cpid);
+ }
+
+ /**
+ * @param mcc 3-digit Mobile Country Code in string format
+ * @param mnc 2 or 3-digit Mobile Network Code in string format
+ * @param lac 16-bit Location Area Code, 0..65535, INT_MAX if unknown
+ * @param cid 28-bit UMTS Cell Identity described in TS 25.331, 0..268435455, INT_MAX if unknown
+ * @param cpid 8-bit Cell Parameters ID described in TS 25.331, 0..127, INT_MAX if unknown
+ *
+ * @hide
+ */
+ public CellIdentityTdscdma(String mcc, String mnc, int lac, int cid, int cpid) {
+ super(TAG, TYPE_TDSCDMA, mcc, mnc);
+ mLac = lac;
+ mCid = cid;
+ mCpid = cpid;
+ }
+
+ private CellIdentityTdscdma(CellIdentityTdscdma cid) {
+ this(cid.mMccStr, cid.mMncStr, cid.mLac, cid.mCid, cid.mCpid);
+ }
+
+ CellIdentityTdscdma copy() {
+ return new CellIdentityTdscdma(this);
+ }
+
+ /**
+ * Get Mobile Country Code in string format
+ * @return Mobile Country Code in string format, null if unknown
+ */
+ public String getMccStr() {
+ return mMccStr;
+ }
+
+ /**
+ * Get Mobile Network Code in string format
+ * @return Mobile Network Code in string format, null if unknown
+ */
+ public String getMncStr() {
+ return mMncStr;
+ }
+
+ /**
+ * @return 16-bit Location Area Code, 0..65535, INT_MAX if unknown
+ */
+ public int getLac() {
+ return mLac;
+ }
+
+ /**
+ * @return 28-bit UMTS Cell Identity described in TS 25.331, 0..268435455, INT_MAX if unknown
+ */
+ public int getCid() {
+ return mCid;
+ }
+
+ /**
+ * @return 8-bit Cell Parameters ID described in TS 25.331, 0..127, INT_MAX if unknown
+ */
+ public int getCpid() {
+ return mCpid;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mMccStr, mMncStr, mLac, mCid, mCpid);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (!(other instanceof CellIdentityTdscdma)) {
+ return false;
+ }
+
+ CellIdentityTdscdma o = (CellIdentityTdscdma) other;
+ return TextUtils.equals(mMccStr, o.mMccStr)
+ && TextUtils.equals(mMncStr, o.mMncStr)
+ && mLac == o.mLac
+ && mCid == o.mCid
+ && mCpid == o.mCpid;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder(TAG)
+ .append(":{ mMcc=").append(mMccStr)
+ .append(" mMnc=").append(mMncStr)
+ .append(" mLac=").append(mLac)
+ .append(" mCid=").append(mCid)
+ .append(" mCpid=").append(mCpid)
+ .append("}").toString();
+ }
+
+ /** Implement the Parcelable interface */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (DBG) log("writeToParcel(Parcel, int): " + toString());
+ super.writeToParcel(dest, TYPE_TDSCDMA);
+ dest.writeInt(mLac);
+ dest.writeInt(mCid);
+ dest.writeInt(mCpid);
+ }
+
+ /** Construct from Parcel, type has already been processed */
+ private CellIdentityTdscdma(Parcel in) {
+ super(TAG, TYPE_TDSCDMA, in);
+ mLac = in.readInt();
+ mCid = in.readInt();
+ mCpid = in.readInt();
+
+ if (DBG) log(toString());
+ }
+
+ /** Implement the Parcelable interface */
+ @SuppressWarnings("hiding")
+ public static final Creator<CellIdentityTdscdma> CREATOR =
+ new Creator<CellIdentityTdscdma>() {
+ @Override
+ public CellIdentityTdscdma createFromParcel(Parcel in) {
+ in.readInt(); // skip
+ return createFromParcelBody(in);
+ }
+
+ @Override
+ public CellIdentityTdscdma[] newArray(int size) {
+ return new CellIdentityTdscdma[size];
+ }
+ };
+
+ /** @hide */
+ protected static CellIdentityTdscdma createFromParcelBody(Parcel in) {
+ return new CellIdentityTdscdma(in);
+ }
+}
diff --git a/android/telephony/CellIdentityWcdma.java b/android/telephony/CellIdentityWcdma.java
index e4bb4f2..1aa1715 100644
--- a/android/telephony/CellIdentityWcdma.java
+++ b/android/telephony/CellIdentityWcdma.java
@@ -17,8 +17,6 @@
package android.telephony;
import android.os.Parcel;
-import android.os.Parcelable;
-import android.telephony.Rlog;
import android.text.TextUtils;
import java.util.Objects;
@@ -26,9 +24,8 @@
/**
* CellIdentity to represent a unique UMTS cell
*/
-public final class CellIdentityWcdma implements Parcelable {
-
- private static final String LOG_TAG = "CellIdentityWcdma";
+public final class CellIdentityWcdma extends CellIdentity {
+ private static final String TAG = CellIdentityWcdma.class.getSimpleName();
private static final boolean DBG = false;
// 16-bit Location Area Code, 0..65535
@@ -39,10 +36,6 @@
private final int mPsc;
// 16-bit UMTS Absolute RF Channel Number
private final int mUarfcn;
- // 3-digit Mobile Country Code in string format
- private final String mMccStr;
- // 2 or 3-digit Mobile Network Code in string format
- private final String mMncStr;
// long alpha Operator Name String or Enhanced Operator Name String
private final String mAlphaLong;
// short alpha Operator Name String or Enhanced Operator Name String
@@ -52,12 +45,11 @@
* @hide
*/
public CellIdentityWcdma() {
+ super(TAG, TYPE_TDSCDMA, null, null);
mLac = Integer.MAX_VALUE;
mCid = Integer.MAX_VALUE;
mPsc = Integer.MAX_VALUE;
mUarfcn = Integer.MAX_VALUE;
- mMccStr = null;
- mMncStr = null;
mAlphaLong = null;
mAlphaShort = null;
}
@@ -106,36 +98,11 @@
*/
public CellIdentityWcdma (int lac, int cid, int psc, int uarfcn,
String mccStr, String mncStr, String alphal, String alphas) {
+ super(TAG, TYPE_WCDMA, mccStr, mncStr);
mLac = lac;
mCid = cid;
mPsc = psc;
mUarfcn = uarfcn;
-
- // Only allow INT_MAX if unknown string mcc/mnc
- if (mccStr == null || mccStr.matches("^[0-9]{3}$")) {
- mMccStr = mccStr;
- } else if (mccStr.isEmpty() || mccStr.equals(String.valueOf(Integer.MAX_VALUE))) {
- // If the mccStr is empty or unknown, set it as null.
- mMccStr = null;
- } else {
- // TODO: b/69384059 Should throw IllegalArgumentException for the invalid MCC format
- // after the bug got fixed.
- mMccStr = null;
- log("invalid MCC format: " + mccStr);
- }
-
- if (mncStr == null || mncStr.matches("^[0-9]{2,3}$")) {
- mMncStr = mncStr;
- } else if (mncStr.isEmpty() || mncStr.equals(String.valueOf(Integer.MAX_VALUE))) {
- // If the mncStr is empty or unknown, set it as null.
- mMncStr = null;
- } else {
- // TODO: b/69384059 Should throw IllegalArgumentException for the invalid MNC format
- // after the bug got fixed.
- mMncStr = null;
- log("invalid MNC format: " + mncStr);
- }
-
mAlphaLong = alphal;
mAlphaShort = alphas;
}
@@ -250,58 +217,53 @@
}
CellIdentityWcdma o = (CellIdentityWcdma) other;
- return mLac == o.mLac &&
- mCid == o.mCid &&
- mPsc == o.mPsc &&
- mUarfcn == o.mUarfcn &&
- TextUtils.equals(mMccStr, o.mMccStr) &&
- TextUtils.equals(mMncStr, o.mMncStr) &&
- TextUtils.equals(mAlphaLong, o.mAlphaLong) &&
- TextUtils.equals(mAlphaShort, o.mAlphaShort);
+ return mLac == o.mLac
+ && mCid == o.mCid
+ && mPsc == o.mPsc
+ && mUarfcn == o.mUarfcn
+ && TextUtils.equals(mMccStr, o.mMccStr)
+ && TextUtils.equals(mMncStr, o.mMncStr)
+ && TextUtils.equals(mAlphaLong, o.mAlphaLong)
+ && TextUtils.equals(mAlphaShort, o.mAlphaShort);
}
@Override
public String toString() {
- StringBuilder sb = new StringBuilder("CellIdentityWcdma:{");
- sb.append(" mLac=").append(mLac);
- sb.append(" mCid=").append(mCid);
- sb.append(" mPsc=").append(mPsc);
- sb.append(" mUarfcn=").append(mUarfcn);
- sb.append(" mMcc=").append(mMccStr);
- sb.append(" mMnc=").append(mMncStr);
- sb.append(" mAlphaLong=").append(mAlphaLong);
- sb.append(" mAlphaShort=").append(mAlphaShort);
- sb.append("}");
-
- return sb.toString();
- }
-
- /** Implement the Parcelable interface */
- @Override
- public int describeContents() {
- return 0;
+ return new StringBuilder(TAG)
+ .append(":{ mLac=").append(mLac)
+ .append(" mCid=").append(mCid)
+ .append(" mPsc=").append(mPsc)
+ .append(" mUarfcn=").append(mUarfcn)
+ .append(" mMcc=").append(mMccStr)
+ .append(" mMnc=").append(mMncStr)
+ .append(" mAlphaLong=").append(mAlphaLong)
+ .append(" mAlphaShort=").append(mAlphaShort)
+ .append("}").toString();
}
/** Implement the Parcelable interface */
@Override
public void writeToParcel(Parcel dest, int flags) {
if (DBG) log("writeToParcel(Parcel, int): " + toString());
+ super.writeToParcel(dest, TYPE_WCDMA);
dest.writeInt(mLac);
dest.writeInt(mCid);
dest.writeInt(mPsc);
dest.writeInt(mUarfcn);
- dest.writeString(mMccStr);
- dest.writeString(mMncStr);
dest.writeString(mAlphaLong);
dest.writeString(mAlphaShort);
}
/** Construct from Parcel, type has already been processed */
private CellIdentityWcdma(Parcel in) {
- this(in.readInt(), in.readInt(), in.readInt(), in.readInt(), in.readString(),
- in.readString(), in.readString(), in.readString());
-
- if (DBG) log("CellIdentityWcdma(Parcel): " + toString());
+ super(TAG, TYPE_WCDMA, in);
+ mLac = in.readInt();
+ mCid = in.readInt();
+ mPsc = in.readInt();
+ mUarfcn = in.readInt();
+ mAlphaLong = in.readString();
+ mAlphaShort = in.readString();
+ if (DBG) log(toString());
}
/** Implement the Parcelable interface */
@@ -310,7 +272,8 @@
new Creator<CellIdentityWcdma>() {
@Override
public CellIdentityWcdma createFromParcel(Parcel in) {
- return new CellIdentityWcdma(in);
+ in.readInt(); // skip
+ return createFromParcelBody(in);
}
@Override
@@ -319,10 +282,8 @@
}
};
- /**
- * log
- */
- private static void log(String s) {
- Rlog.w(LOG_TAG, s);
+ /** @hide */
+ protected static CellIdentityWcdma createFromParcelBody(Parcel in) {
+ return new CellIdentityWcdma(in);
}
}
\ No newline at end of file
diff --git a/android/telephony/DisconnectCause.java b/android/telephony/DisconnectCause.java
index 56e1e64..4fa304a 100644
--- a/android/telephony/DisconnectCause.java
+++ b/android/telephony/DisconnectCause.java
@@ -310,6 +310,13 @@
* {@hide}
*/
public static final int DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO = 70;
+
+ /**
+ * The network has reported that an alternative emergency number has been dialed, but the user
+ * must exit airplane mode to place the call.
+ */
+ public static final int IMS_SIP_ALTERNATE_EMERGENCY_CALL = 71;
+
//*********************************************************************************************
// When adding a disconnect type:
// 1) Update toString() with the newly added disconnect type.
@@ -462,6 +469,8 @@
return "EMERGENCY_PERM_FAILURE";
case NORMAL_UNSPECIFIED:
return "NORMAL_UNSPECIFIED";
+ case IMS_SIP_ALTERNATE_EMERGENCY_CALL:
+ return "IMS_SIP_ALTERNATE_EMERGENCY_CALL";
default:
return "INVALID: " + cause;
}
diff --git a/android/telephony/NetworkRegistrationState.java b/android/telephony/NetworkRegistrationState.java
new file mode 100644
index 0000000..e051069
--- /dev/null
+++ b/android/telephony/NetworkRegistrationState.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2017 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.telephony;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Description of a mobile network registration state
+ * @hide
+ */
+@SystemApi
+public class NetworkRegistrationState implements Parcelable {
+ /**
+ * Network domain
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "DOMAIN_", value = {DOMAIN_CS, DOMAIN_PS})
+ public @interface Domain {}
+
+ /** Circuit switching domain */
+ public static final int DOMAIN_CS = 1;
+ /** Packet switching domain */
+ public static final int DOMAIN_PS = 2;
+
+ /**
+ * Registration state
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "REG_STATE_",
+ value = {REG_STATE_NOT_REG_NOT_SEARCHING, REG_STATE_HOME, REG_STATE_NOT_REG_SEARCHING,
+ REG_STATE_DENIED, REG_STATE_UNKNOWN, REG_STATE_ROAMING})
+ public @interface RegState {}
+
+ /** Not registered. The device is not currently searching a new operator to register */
+ public static final int REG_STATE_NOT_REG_NOT_SEARCHING = 0;
+ /** Registered on home network */
+ public static final int REG_STATE_HOME = 1;
+ /** Not registered. The device is currently searching a new operator to register */
+ public static final int REG_STATE_NOT_REG_SEARCHING = 2;
+ /** Registration denied */
+ public static final int REG_STATE_DENIED = 3;
+ /** Registration state is unknown */
+ public static final int REG_STATE_UNKNOWN = 4;
+ /** Registered on roaming network */
+ public static final int REG_STATE_ROAMING = 5;
+
+ /**
+ * Supported service type
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "SERVICE_TYPE_",
+ value = {SERVICE_TYPE_VOICE, SERVICE_TYPE_DATA, SERVICE_TYPE_SMS, SERVICE_TYPE_VIDEO,
+ SERVICE_TYPE_EMERGENCY})
+ public @interface ServiceType {}
+
+ public static final int SERVICE_TYPE_VOICE = 1;
+ public static final int SERVICE_TYPE_DATA = 2;
+ public static final int SERVICE_TYPE_SMS = 3;
+ public static final int SERVICE_TYPE_VIDEO = 4;
+ public static final int SERVICE_TYPE_EMERGENCY = 5;
+
+ /** {@link AccessNetworkConstants.TransportType}*/
+ private final int mTransportType;
+
+ @Domain
+ private final int mDomain;
+
+ @RegState
+ private final int mRegState;
+
+ private final int mAccessNetworkTechnology;
+
+ private final int mReasonForDenial;
+
+ private final boolean mEmergencyOnly;
+
+ private final int[] mAvailableServices;
+
+ @Nullable
+ private final CellIdentity mCellIdentity;
+
+
+ /**
+ * @param transportType Transport type. Must be {@link AccessNetworkConstants.TransportType}
+ * @param domain Network domain. Must be DOMAIN_CS or DOMAIN_PS.
+ * @param regState Network registration state.
+ * @param accessNetworkTechnology See TelephonyManager NETWORK_TYPE_XXXX.
+ * @param reasonForDenial Reason for denial if the registration state is DENIED.
+ * @param availableServices The supported service.
+ * @param cellIdentity The identity representing a unique cell
+ */
+ public NetworkRegistrationState(int transportType, int domain, int regState,
+ int accessNetworkTechnology, int reasonForDenial, boolean emergencyOnly,
+ int[] availableServices, @Nullable CellIdentity cellIdentity) {
+ mTransportType = transportType;
+ mDomain = domain;
+ mRegState = regState;
+ mAccessNetworkTechnology = accessNetworkTechnology;
+ mReasonForDenial = reasonForDenial;
+ mAvailableServices = availableServices;
+ mCellIdentity = cellIdentity;
+ mEmergencyOnly = emergencyOnly;
+ }
+
+ protected NetworkRegistrationState(Parcel source) {
+ mTransportType = source.readInt();
+ mDomain = source.readInt();
+ mRegState = source.readInt();
+ mAccessNetworkTechnology = source.readInt();
+ mReasonForDenial = source.readInt();
+ mEmergencyOnly = source.readBoolean();
+ mAvailableServices = source.createIntArray();
+ mCellIdentity = source.readParcelable(CellIdentity.class.getClassLoader());
+ }
+
+ /**
+ * @return The transport type.
+ */
+ public int getTransportType() { return mTransportType; }
+
+ /**
+ * @return The network domain.
+ */
+ public @Domain int getDomain() { return mDomain; }
+
+ /**
+ * @return The registration state.
+ */
+ public @RegState int getRegState() {
+ return mRegState;
+ }
+
+ /**
+ * @return Whether emergency is enabled.
+ */
+ public boolean isEmergencyEnabled() { return mEmergencyOnly; }
+
+ /**
+ * @return List of available service types.
+ */
+ public int[] getAvailableServices() { return mAvailableServices; }
+
+ /**
+ * @return The access network technology. Must be one of TelephonyManager.NETWORK_TYPE_XXXX.
+ */
+ public int getAccessNetworkTechnology() {
+ return mAccessNetworkTechnology;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ private static String regStateToString(int regState) {
+ switch (regState) {
+ case REG_STATE_NOT_REG_NOT_SEARCHING: return "NOT_REG_NOT_SEARCHING";
+ case REG_STATE_HOME: return "HOME";
+ case REG_STATE_NOT_REG_SEARCHING: return "NOT_REG_SEARCHING";
+ case REG_STATE_DENIED: return "DENIED";
+ case REG_STATE_UNKNOWN: return "UNKNOWN";
+ case REG_STATE_ROAMING: return "ROAMING";
+ }
+ return "Unknown reg state " + regState;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder("NetworkRegistrationState{")
+ .append("transportType=").append(mTransportType)
+ .append(" domain=").append((mDomain == DOMAIN_CS) ? "CS" : "PS")
+ .append(" regState=").append(regStateToString(mRegState))
+ .append(" accessNetworkTechnology=")
+ .append(TelephonyManager.getNetworkTypeName(mAccessNetworkTechnology))
+ .append(" reasonForDenial=").append(mReasonForDenial)
+ .append(" emergencyEnabled=").append(mEmergencyOnly)
+ .append(" supportedServices=").append(mAvailableServices)
+ .append(" cellIdentity=").append(mCellIdentity)
+ .append("}").toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTransportType, mDomain, mRegState, mAccessNetworkTechnology,
+ mReasonForDenial, mEmergencyOnly, mAvailableServices, mCellIdentity);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+
+ if (o == null || !(o instanceof NetworkRegistrationState)) {
+ return false;
+ }
+
+ NetworkRegistrationState other = (NetworkRegistrationState) o;
+ return mTransportType == other.mTransportType
+ && mDomain == other.mDomain
+ && mRegState == other.mRegState
+ && mAccessNetworkTechnology == other.mAccessNetworkTechnology
+ && mReasonForDenial == other.mReasonForDenial
+ && mEmergencyOnly == other.mEmergencyOnly
+ && (mAvailableServices == other.mAvailableServices
+ || Arrays.equals(mAvailableServices, other.mAvailableServices))
+ && mCellIdentity == other.mCellIdentity;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mTransportType);
+ dest.writeInt(mDomain);
+ dest.writeInt(mRegState);
+ dest.writeInt(mAccessNetworkTechnology);
+ dest.writeInt(mReasonForDenial);
+ dest.writeBoolean(mEmergencyOnly);
+ dest.writeIntArray(mAvailableServices);
+ dest.writeParcelable(mCellIdentity, 0);
+ }
+
+ public static final Parcelable.Creator<NetworkRegistrationState> CREATOR =
+ new Parcelable.Creator<NetworkRegistrationState>() {
+ @Override
+ public NetworkRegistrationState createFromParcel(Parcel source) {
+ return new NetworkRegistrationState(source);
+ }
+
+ @Override
+ public NetworkRegistrationState[] newArray(int size) {
+ return new NetworkRegistrationState[size];
+ }
+ };
+}
diff --git a/android/telephony/NetworkScanRequest.java b/android/telephony/NetworkScanRequest.java
index ea503c3..9726569 100644
--- a/android/telephony/NetworkScanRequest.java
+++ b/android/telephony/NetworkScanRequest.java
@@ -143,7 +143,11 @@
int incrementalResultsPeriodicity,
ArrayList<String> mccMncs) {
this.mScanType = scanType;
- this.mSpecifiers = specifiers.clone();
+ if (specifiers != null) {
+ this.mSpecifiers = specifiers.clone();
+ } else {
+ this.mSpecifiers = null;
+ }
this.mSearchPeriodicity = searchPeriodicity;
this.mMaxSearchTime = maxSearchTime;
this.mIncrementalResults = incrementalResults;
@@ -187,7 +191,7 @@
/** Returns the radio access technologies with bands or channels that need to be scanned. */
public RadioAccessSpecifier[] getSpecifiers() {
- return mSpecifiers.clone();
+ return mSpecifiers == null ? null : mSpecifiers.clone();
}
/**
diff --git a/android/telephony/NetworkService.java b/android/telephony/NetworkService.java
new file mode 100644
index 0000000..6b3584c
--- /dev/null
+++ b/android/telephony/NetworkService.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2017 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.telephony;
+
+import android.annotation.CallSuper;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base class of network service. Services that extend NetworkService must register the service in
+ * their AndroidManifest to be detected by the framework. They must be protected by the permission
+ * "android.permission.BIND_NETWORK_SERVICE". The network service definition in the manifest must
+ * follow the following format:
+ * ...
+ * <service android:name=".xxxNetworkService"
+ * android:permission="android.permission.BIND_NETWORK_SERVICE" >
+ * <intent-filter>
+ * <action android:name="android.telephony.NetworkService" />
+ * </intent-filter>
+ * </service>
+ * @hide
+ */
+@SystemApi
+public abstract class NetworkService extends Service {
+
+ private final String TAG = NetworkService.class.getSimpleName();
+
+ public static final String NETWORK_SERVICE_INTERFACE = "android.telephony.NetworkService";
+ public static final String NETWORK_SERVICE_EXTRA_SLOT_ID = "android.telephony.extra.SLOT_ID";
+
+ private static final int NETWORK_SERVICE_INTERNAL_REQUEST_INITIALIZE_SERVICE = 1;
+ private static final int NETWORK_SERVICE_GET_REGISTRATION_STATE = 2;
+ private static final int NETWORK_SERVICE_REGISTER_FOR_STATE_CHANGE = 3;
+ private static final int NETWORK_SERVICE_UNREGISTER_FOR_STATE_CHANGE = 4;
+ private static final int NETWORK_SERVICE_INDICATION_NETWORK_STATE_CHANGED = 5;
+
+
+ private final HandlerThread mHandlerThread;
+
+ private final NetworkServiceHandler mHandler;
+
+ private final SparseArray<NetworkServiceProvider> mServiceMap = new SparseArray<>();
+
+ private final SparseArray<INetworkServiceWrapper> mBinderMap = new SparseArray<>();
+
+ /**
+ * The abstract class of the actual network service implementation. The network service provider
+ * must extend this class to support network connection. Note that each instance of network
+ * service is associated with one physical SIM slot.
+ */
+ public class NetworkServiceProvider {
+ private final int mSlotId;
+
+ private final List<INetworkServiceCallback>
+ mNetworkRegistrationStateChangedCallbacks = new ArrayList<>();
+
+ public NetworkServiceProvider(int slotId) {
+ mSlotId = slotId;
+ }
+
+ /**
+ * @return SIM slot id the network service associated with.
+ */
+ public final int getSlotId() {
+ return mSlotId;
+ }
+
+ /**
+ * API to get network registration state. The result will be passed to the callback.
+ * @param domain
+ * @param callback
+ * @return SIM slot id the network service associated with.
+ */
+ public void getNetworkRegistrationState(int domain, NetworkServiceCallback callback) {
+ callback.onGetNetworkRegistrationStateComplete(
+ NetworkServiceCallback.RESULT_ERROR_UNSUPPORTED, null);
+ }
+
+ public final void notifyNetworkRegistrationStateChanged() {
+ mHandler.obtainMessage(NETWORK_SERVICE_INDICATION_NETWORK_STATE_CHANGED,
+ mSlotId, 0, null).sendToTarget();
+ }
+
+ private void registerForStateChanged(INetworkServiceCallback callback) {
+ synchronized (mNetworkRegistrationStateChangedCallbacks) {
+ mNetworkRegistrationStateChangedCallbacks.add(callback);
+ }
+ }
+
+ private void unregisterForStateChanged(INetworkServiceCallback callback) {
+ synchronized (mNetworkRegistrationStateChangedCallbacks) {
+ mNetworkRegistrationStateChangedCallbacks.remove(callback);
+ }
+ }
+
+ private void notifyStateChangedToCallbacks() {
+ for (INetworkServiceCallback callback : mNetworkRegistrationStateChangedCallbacks) {
+ try {
+ callback.onNetworkStateChanged();
+ } catch (RemoteException exception) {
+ // Doing nothing.
+ }
+ }
+ }
+
+ /**
+ * Called when the instance of network service is destroyed (e.g. got unbind or binder died).
+ */
+ @CallSuper
+ protected void onDestroy() {
+ mNetworkRegistrationStateChangedCallbacks.clear();
+ }
+ }
+
+ private class NetworkServiceHandler extends Handler {
+
+ NetworkServiceHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ final int slotId = message.arg1;
+ final INetworkServiceCallback callback = (INetworkServiceCallback) message.obj;
+ NetworkServiceProvider service;
+
+ synchronized (mServiceMap) {
+ service = mServiceMap.get(slotId);
+ }
+
+ switch (message.what) {
+ case NETWORK_SERVICE_INTERNAL_REQUEST_INITIALIZE_SERVICE:
+ service = createNetworkServiceProvider(message.arg1);
+ if (service != null) {
+ mServiceMap.put(slotId, service);
+ }
+ break;
+ case NETWORK_SERVICE_GET_REGISTRATION_STATE:
+ if (service == null) break;
+ int domainId = message.arg2;
+ service.getNetworkRegistrationState(domainId,
+ new NetworkServiceCallback(callback));
+
+ break;
+ case NETWORK_SERVICE_REGISTER_FOR_STATE_CHANGE:
+ if (service == null) break;
+ service.registerForStateChanged(callback);
+ break;
+ case NETWORK_SERVICE_UNREGISTER_FOR_STATE_CHANGE:
+ if (service == null) break;
+ service.unregisterForStateChanged(callback);
+ break;
+ case NETWORK_SERVICE_INDICATION_NETWORK_STATE_CHANGED:
+ if (service == null) break;
+ service.notifyStateChangedToCallbacks();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ /** @hide */
+ protected NetworkService() {
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+
+ mHandler = new NetworkServiceHandler(mHandlerThread.getLooper());
+ log("network service created");
+ }
+
+ /**
+ * Create the instance of {@link NetworkServiceProvider}. Network service provider must override
+ * this method to facilitate the creation of {@link NetworkServiceProvider} instances. The system
+ * will call this method after binding the network service for each active SIM slot id.
+ *
+ * @param slotId SIM slot id the network service associated with.
+ * @return Network service object
+ */
+ protected abstract NetworkServiceProvider createNetworkServiceProvider(int slotId);
+
+ /** @hide */
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (intent == null || !NETWORK_SERVICE_INTERFACE.equals(intent.getAction())) {
+ loge("Unexpected intent " + intent);
+ return null;
+ }
+
+ int slotId = intent.getIntExtra(
+ NETWORK_SERVICE_EXTRA_SLOT_ID, SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+
+ if (!SubscriptionManager.isValidSlotIndex(slotId)) {
+ loge("Invalid slot id " + slotId);
+ return null;
+ }
+
+ log("onBind: slot id=" + slotId);
+
+ INetworkServiceWrapper binder = mBinderMap.get(slotId);
+ if (binder == null) {
+ Message msg = mHandler.obtainMessage(
+ NETWORK_SERVICE_INTERNAL_REQUEST_INITIALIZE_SERVICE);
+ msg.arg1 = slotId;
+ msg.sendToTarget();
+
+ binder = new INetworkServiceWrapper(slotId);
+ mBinderMap.put(slotId, binder);
+ }
+
+ return binder;
+ }
+
+ /** @hide */
+ @Override
+ public boolean onUnbind(Intent intent) {
+ int slotId = intent.getIntExtra(NETWORK_SERVICE_EXTRA_SLOT_ID,
+ SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+ if (mBinderMap.get(slotId) != null) {
+ NetworkServiceProvider serviceImpl;
+ synchronized (mServiceMap) {
+ serviceImpl = mServiceMap.get(slotId);
+ }
+ // We assume only one component might bind to the service. So if onUnbind is ever
+ // called, we destroy the serviceImpl.
+ if (serviceImpl != null) {
+ serviceImpl.onDestroy();
+ }
+ mBinderMap.remove(slotId);
+ }
+
+ return false;
+ }
+
+ /** @hide */
+ @Override
+ public void onDestroy() {
+ synchronized (mServiceMap) {
+ for (int i = 0; i < mServiceMap.size(); i++) {
+ NetworkServiceProvider serviceImpl = mServiceMap.get(i);
+ if (serviceImpl != null) {
+ serviceImpl.onDestroy();
+ }
+ }
+ mServiceMap.clear();
+ }
+
+ mHandlerThread.quit();
+ }
+
+ /**
+ * A wrapper around INetworkService that forwards calls to implementations of
+ * {@link NetworkService}.
+ */
+ private class INetworkServiceWrapper extends INetworkService.Stub {
+
+ private final int mSlotId;
+
+ INetworkServiceWrapper(int slotId) {
+ mSlotId = slotId;
+ }
+
+ @Override
+ public void getNetworkRegistrationState(int domain, INetworkServiceCallback callback) {
+ mHandler.obtainMessage(NETWORK_SERVICE_GET_REGISTRATION_STATE, mSlotId,
+ domain, callback).sendToTarget();
+ }
+
+ @Override
+ public void registerForNetworkRegistrationStateChanged(INetworkServiceCallback callback) {
+ mHandler.obtainMessage(NETWORK_SERVICE_REGISTER_FOR_STATE_CHANGE, mSlotId,
+ 0, callback).sendToTarget();
+ }
+
+ @Override
+ public void unregisterForNetworkRegistrationStateChanged(INetworkServiceCallback callback) {
+ mHandler.obtainMessage(NETWORK_SERVICE_UNREGISTER_FOR_STATE_CHANGE, mSlotId,
+ 0, callback).sendToTarget();
+ }
+ }
+
+ private final void log(String s) {
+ Rlog.d(TAG, s);
+ }
+
+ private final void loge(String s) {
+ Rlog.e(TAG, s);
+ }
+}
\ No newline at end of file
diff --git a/android/telephony/NetworkServiceCallback.java b/android/telephony/NetworkServiceCallback.java
new file mode 100644
index 0000000..92ebf36
--- /dev/null
+++ b/android/telephony/NetworkServiceCallback.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2017 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.telephony;
+
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
+import android.os.RemoteException;
+import android.telephony.NetworkService.NetworkServiceProvider;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+
+/**
+ * Network service callback. Object of this class is passed to NetworkServiceProvider upon
+ * calling getNetworkRegistrationState, to receive asynchronous feedback from NetworkServiceProvider
+ * upon onGetNetworkRegistrationStateComplete. It's like a wrapper of INetworkServiceCallback
+ * because INetworkServiceCallback can't be a parameter type in public APIs.
+ *
+ * @hide
+ */
+@SystemApi
+public class NetworkServiceCallback {
+
+ private static final String mTag = NetworkServiceCallback.class.getSimpleName();
+
+ /**
+ * Result of network requests
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({RESULT_SUCCESS, RESULT_ERROR_UNSUPPORTED, RESULT_ERROR_INVALID_ARG, RESULT_ERROR_BUSY,
+ RESULT_ERROR_ILLEGAL_STATE, RESULT_ERROR_FAILED})
+ public @interface Result {}
+
+ /** Request is completed successfully */
+ public static final int RESULT_SUCCESS = 0;
+ /** Request is not support */
+ public static final int RESULT_ERROR_UNSUPPORTED = 1;
+ /** Request contains invalid arguments */
+ public static final int RESULT_ERROR_INVALID_ARG = 2;
+ /** Service is busy */
+ public static final int RESULT_ERROR_BUSY = 3;
+ /** Request sent in illegal state */
+ public static final int RESULT_ERROR_ILLEGAL_STATE = 4;
+ /** Request failed */
+ public static final int RESULT_ERROR_FAILED = 5;
+
+ private final WeakReference<INetworkServiceCallback> mCallback;
+
+ /** @hide */
+ public NetworkServiceCallback(INetworkServiceCallback callback) {
+ mCallback = new WeakReference<>(callback);
+ }
+
+ /**
+ * Called to indicate result of
+ * {@link NetworkServiceProvider#getNetworkRegistrationState(int, NetworkServiceCallback)}
+ *
+ * @param result Result status like {@link NetworkServiceCallback#RESULT_SUCCESS} or
+ * {@link NetworkServiceCallback#RESULT_ERROR_UNSUPPORTED}
+ * @param state The state information to be returned to callback.
+ */
+ public void onGetNetworkRegistrationStateComplete(int result, NetworkRegistrationState state) {
+ INetworkServiceCallback callback = mCallback.get();
+ if (callback != null) {
+ try {
+ callback.onGetNetworkRegistrationStateComplete(result, state);
+ } catch (RemoteException e) {
+ Rlog.e(mTag, "Failed to onGetNetworkRegistrationStateComplete on the remote");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/telephony/PhoneStateListener.java b/android/telephony/PhoneStateListener.java
index c7e5131..0ee870a 100644
--- a/android/telephony/PhoneStateListener.java
+++ b/android/telephony/PhoneStateListener.java
@@ -244,7 +244,22 @@
*/
public static final int LISTEN_DATA_ACTIVATION_STATE = 0x00040000;
- /*
+ /**
+ * Listen for changes to the user mobile data state
+ *
+ * @see #onUserMobileDataStateChanged
+ */
+ public static final int LISTEN_USER_MOBILE_DATA_STATE = 0x00080000;
+
+ /**
+ * Listen for changes to the physical channel configuration.
+ *
+ * @see #onPhysicalChannelConfigurationChanged
+ * @hide
+ */
+ public static final int LISTEN_PHYSICAL_CHANNEL_CONFIGURATION = 0x00100000;
+
+ /*
* Subscription used to listen to the phone state changes
* @hide
*/
@@ -349,10 +364,16 @@
case LISTEN_DATA_ACTIVATION_STATE:
PhoneStateListener.this.onDataActivationStateChanged((int)msg.obj);
break;
+ case LISTEN_USER_MOBILE_DATA_STATE:
+ PhoneStateListener.this.onUserMobileDataStateChanged((boolean)msg.obj);
+ break;
case LISTEN_CARRIER_NETWORK_CHANGE:
PhoneStateListener.this.onCarrierNetworkChange((boolean)msg.obj);
break;
-
+ case LISTEN_PHYSICAL_CHANNEL_CONFIGURATION:
+ PhoneStateListener.this.onPhysicalChannelConfigurationChanged(
+ (List<PhysicalChannelConfig>)msg.obj);
+ break;
}
}
};
@@ -543,6 +564,24 @@
}
/**
+ * Callback invoked when the user mobile data state has changed
+ * @param enabled indicates whether the current user mobile data state is enabled or disabled.
+ */
+ public void onUserMobileDataStateChanged(boolean enabled) {
+ // default implementation empty
+ }
+
+ /**
+ * Callback invoked when the current physical channel configuration has changed
+ *
+ * @param configs List of the current {@link PhysicalChannelConfig}s
+ * @hide
+ */
+ public void onPhysicalChannelConfigurationChanged(List<PhysicalChannelConfig> configs) {
+ // default implementation empty
+ }
+
+ /**
* Callback invoked when telephony has received notice from a carrier
* app that a network action that could result in connectivity loss
* has been requested by an app using
@@ -654,6 +693,10 @@
send(LISTEN_DATA_ACTIVATION_STATE, 0, 0, activationState);
}
+ public void onUserMobileDataStateChanged(boolean enabled) {
+ send(LISTEN_USER_MOBILE_DATA_STATE, 0, 0, enabled);
+ }
+
public void onCarrierNetworkChange(boolean active) {
send(LISTEN_CARRIER_NETWORK_CHANGE, 0, 0, active);
}
diff --git a/android/telephony/PhysicalChannelConfig.java b/android/telephony/PhysicalChannelConfig.java
new file mode 100644
index 0000000..651d68d
--- /dev/null
+++ b/android/telephony/PhysicalChannelConfig.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2018 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.telephony;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * @hide
+ */
+public final class PhysicalChannelConfig implements Parcelable {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({CONNECTION_PRIMARY_SERVING, CONNECTION_SECONDARY_SERVING})
+ public @interface ConnectionStatus {}
+
+ /**
+ * UE has connection to cell for signalling and possibly data (3GPP 36.331, 25.331).
+ */
+ public static final int CONNECTION_PRIMARY_SERVING = 1;
+
+ /**
+ * UE has connection to cell for data (3GPP 36.331, 25.331).
+ */
+ public static final int CONNECTION_SECONDARY_SERVING = 2;
+
+ /**
+ * Connection status of the cell.
+ *
+ * <p>One of {@link #CONNECTION_PRIMARY_SERVING}, {@link #CONNECTION_SECONDARY_SERVING}.
+ */
+ private int mCellConnectionStatus;
+
+ /**
+ * Cell bandwidth, in kHz.
+ */
+ private int mCellBandwidthDownlinkKhz;
+
+ public PhysicalChannelConfig(int status, int bandwidth) {
+ mCellConnectionStatus = status;
+ mCellBandwidthDownlinkKhz = bandwidth;
+ }
+
+ public PhysicalChannelConfig(Parcel in) {
+ mCellConnectionStatus = in.readInt();
+ mCellBandwidthDownlinkKhz = in.readInt();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mCellConnectionStatus);
+ dest.writeInt(mCellBandwidthDownlinkKhz);
+ }
+
+ /**
+ * @return Cell bandwidth, in kHz
+ */
+ public int getCellBandwidthDownlink() {
+ return mCellBandwidthDownlinkKhz;
+ }
+
+ /**
+ * Gets the connection status of the cell.
+ *
+ * @see #CONNECTION_PRIMARY_SERVING
+ * @see #CONNECTION_SECONDARY_SERVING
+ *
+ * @return Connection status of the cell
+ */
+ @ConnectionStatus
+ public int getConnectionStatus() {
+ return mCellConnectionStatus;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof PhysicalChannelConfig)) {
+ return false;
+ }
+
+ PhysicalChannelConfig config = (PhysicalChannelConfig) o;
+ return mCellConnectionStatus == config.mCellConnectionStatus
+ && mCellBandwidthDownlinkKhz == config.mCellBandwidthDownlinkKhz;
+ }
+
+ @Override
+ public int hashCode() {
+ return (mCellBandwidthDownlinkKhz * 29) + (mCellConnectionStatus * 31);
+ }
+
+ public static final Parcelable.Creator<PhysicalChannelConfig> CREATOR =
+ new Parcelable.Creator<PhysicalChannelConfig>() {
+ public PhysicalChannelConfig createFromParcel(Parcel in) {
+ return new PhysicalChannelConfig(in);
+ }
+
+ public PhysicalChannelConfig[] newArray(int size) {
+ return new PhysicalChannelConfig[size];
+ }
+ };
+}
diff --git a/android/telephony/RadioAccessSpecifier.java b/android/telephony/RadioAccessSpecifier.java
index 5412c61..81e7ed0 100644
--- a/android/telephony/RadioAccessSpecifier.java
+++ b/android/telephony/RadioAccessSpecifier.java
@@ -33,7 +33,7 @@
*
* This parameter must be provided or else the scan will be rejected.
*
- * See {@link RadioNetworkConstants.RadioAccessNetworks} for details.
+ * See {@link AccessNetworkConstants.AccessNetworkType} for details.
*/
private int mRadioAccessNetwork;
@@ -43,7 +43,7 @@
* When no specific bands are specified (empty array or null), all the frequency bands
* supported by the modem will be scanned.
*
- * See {@link RadioNetworkConstants} for details.
+ * See {@link AccessNetworkConstants} for details.
*/
private int[] mBands;
@@ -56,7 +56,7 @@
* When no specific channels are specified (empty array or null), all the frequency channels
* supported by the modem will be scanned.
*
- * See {@link RadioNetworkConstants} for details.
+ * See {@link AccessNetworkConstants} for details.
*/
private int[] mChannels;
@@ -72,14 +72,22 @@
*/
public RadioAccessSpecifier(int ran, int[] bands, int[] channels) {
this.mRadioAccessNetwork = ran;
- this.mBands = bands.clone();
- this.mChannels = channels.clone();
+ if (bands != null) {
+ this.mBands = bands.clone();
+ } else {
+ this.mBands = null;
+ }
+ if (channels != null) {
+ this.mChannels = channels.clone();
+ } else {
+ this.mChannels = null;
+ }
}
/**
* Returns the radio access network that needs to be scanned.
*
- * The returned value is define in {@link RadioNetworkConstants.RadioAccessNetworks};
+ * The returned value is define in {@link AccessNetworkConstants.AccessNetworkType};
*/
public int getRadioAccessNetwork() {
return mRadioAccessNetwork;
@@ -88,17 +96,17 @@
/**
* Returns the frequency bands that need to be scanned.
*
- * The returned value is defined in either of {@link RadioNetworkConstants.GeranBands},
- * {@link RadioNetworkConstants.UtranBands} and {@link RadioNetworkConstants.EutranBands}, and
+ * The returned value is defined in either of {@link AccessNetworkConstants.GeranBand},
+ * {@link AccessNetworkConstants.UtranBand} and {@link AccessNetworkConstants.EutranBand}, and
* it depends on the returned value of {@link #getRadioAccessNetwork()}.
*/
public int[] getBands() {
- return mBands.clone();
+ return mBands == null ? null : mBands.clone();
}
/** Returns the frequency channels that need to be scanned. */
public int[] getChannels() {
- return mChannels.clone();
+ return mChannels == null ? null : mChannels.clone();
}
public static final Parcelable.Creator<RadioAccessSpecifier> CREATOR =
diff --git a/android/telephony/ServiceState.java b/android/telephony/ServiceState.java
index 116e711..77706e8 100644
--- a/android/telephony/ServiceState.java
+++ b/android/telephony/ServiceState.java
@@ -16,12 +16,19 @@
package android.telephony;
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
-import android.telephony.Rlog;
import android.text.TextUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Contains phone state and service related information.
*
@@ -105,6 +112,31 @@
/** @hide */
public static final int RIL_REG_STATE_UNKNOWN_EMERGENCY_CALL_ENABLED = 14;
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "RIL_RADIO_TECHNOLOGY_" },
+ value = {
+ RIL_RADIO_TECHNOLOGY_UNKNOWN,
+ RIL_RADIO_TECHNOLOGY_GPRS,
+ RIL_RADIO_TECHNOLOGY_EDGE,
+ RIL_RADIO_TECHNOLOGY_UMTS,
+ RIL_RADIO_TECHNOLOGY_IS95A,
+ RIL_RADIO_TECHNOLOGY_IS95B,
+ RIL_RADIO_TECHNOLOGY_1xRTT,
+ RIL_RADIO_TECHNOLOGY_EVDO_0,
+ RIL_RADIO_TECHNOLOGY_EVDO_A,
+ RIL_RADIO_TECHNOLOGY_HSDPA,
+ RIL_RADIO_TECHNOLOGY_HSUPA,
+ RIL_RADIO_TECHNOLOGY_HSPA,
+ RIL_RADIO_TECHNOLOGY_EVDO_B,
+ RIL_RADIO_TECHNOLOGY_EHRPD,
+ RIL_RADIO_TECHNOLOGY_LTE,
+ RIL_RADIO_TECHNOLOGY_HSPAP,
+ RIL_RADIO_TECHNOLOGY_GSM,
+ RIL_RADIO_TECHNOLOGY_TD_SCDMA,
+ RIL_RADIO_TECHNOLOGY_IWLAN,
+ RIL_RADIO_TECHNOLOGY_LTE_CA})
+ public @interface RilRadioTechnology {}
/**
* Available radio technologies for GSM, UMTS and CDMA.
* Duplicates the constants from hardware/radio/include/ril.h
@@ -162,6 +194,12 @@
*/
public static final int RIL_RADIO_TECHNOLOGY_LTE_CA = 19;
+ /**
+ * Number of radio technologies for GSM, UMTS and CDMA.
+ * @hide
+ */
+ private static final int NEXT_RIL_RADIO_TECHNOLOGY = 20;
+
/** @hide */
public static final int RIL_RADIO_CDMA_TECHNOLOGY_BITMASK =
(1 << (RIL_RADIO_TECHNOLOGY_IS95A - 1))
@@ -216,6 +254,11 @@
*/
public static final int ROAMING_TYPE_INTERNATIONAL = 3;
+ /**
+ * Unknown ID. Could be returned by {@link #getNetworkId()} or {@link #getSystemId()}
+ */
+ public static final int UNKNOWN_ID = -1;
+
private int mVoiceRoamingType;
private int mDataRoamingType;
private String mVoiceOperatorAlphaLong;
@@ -247,6 +290,8 @@
* Reference: 3GPP TS 36.104 5.4.3 */
private int mLteEarfcnRsrpBoost = 0;
+ private List<NetworkRegistrationState> mNetworkRegistrationStates = new ArrayList<>();
+
/**
* get String description of roaming type
* @hide
@@ -327,6 +372,7 @@
mIsDataRoamingFromRegistration = s.mIsDataRoamingFromRegistration;
mIsUsingCarrierAggregation = s.mIsUsingCarrierAggregation;
mLteEarfcnRsrpBoost = s.mLteEarfcnRsrpBoost;
+ mNetworkRegistrationStates = new ArrayList<>(s.mNetworkRegistrationStates);
}
/**
@@ -357,6 +403,8 @@
mIsDataRoamingFromRegistration = in.readInt() != 0;
mIsUsingCarrierAggregation = in.readInt() != 0;
mLteEarfcnRsrpBoost = in.readInt();
+ mNetworkRegistrationStates = new ArrayList<>();
+ in.readList(mNetworkRegistrationStates, NetworkRegistrationState.class.getClassLoader());
}
public void writeToParcel(Parcel out, int flags) {
@@ -384,6 +432,7 @@
out.writeInt(mIsDataRoamingFromRegistration ? 1 : 0);
out.writeInt(mIsUsingCarrierAggregation ? 1 : 0);
out.writeInt(mLteEarfcnRsrpBoost);
+ out.writeList(mNetworkRegistrationStates);
}
public int describeContents() {
@@ -712,13 +761,14 @@
s.mCdmaDefaultRoamingIndicator)
&& mIsEmergencyOnly == s.mIsEmergencyOnly
&& mIsDataRoamingFromRegistration == s.mIsDataRoamingFromRegistration
- && mIsUsingCarrierAggregation == s.mIsUsingCarrierAggregation);
+ && mIsUsingCarrierAggregation == s.mIsUsingCarrierAggregation)
+ && mNetworkRegistrationStates.containsAll(s.mNetworkRegistrationStates);
}
/**
* Convert radio technology to String
*
- * @param radioTechnology
+ * @param rt radioTechnology
* @return String representation of the RAT
*
* @hide
@@ -845,6 +895,7 @@
.append(", mIsDataRoamingFromRegistration=").append(mIsDataRoamingFromRegistration)
.append(", mIsUsingCarrierAggregation=").append(mIsUsingCarrierAggregation)
.append(", mLteEarfcnRsrpBoost=").append(mLteEarfcnRsrpBoost)
+ .append(", mNetworkRegistrationStates=").append(mNetworkRegistrationStates)
.append("}").toString();
}
@@ -874,6 +925,7 @@
mIsDataRoamingFromRegistration = false;
mIsUsingCarrierAggregation = false;
mLteEarfcnRsrpBoost = 0;
+ mNetworkRegistrationStates = new ArrayList<>();
}
public void setStateOutOfService() {
@@ -1153,7 +1205,8 @@
return getRilDataRadioTechnology();
}
- private int rilRadioTechnologyToNetworkType(int rt) {
+ /** @hide */
+ public static int rilRadioTechnologyToNetworkType(@RilRadioTechnology int rt) {
switch(rt) {
case ServiceState.RIL_RADIO_TECHNOLOGY_GPRS:
return TelephonyManager.NETWORK_TYPE_GPRS;
@@ -1212,12 +1265,20 @@
return this.mCssIndicator ? 1 : 0;
}
- /** @hide */
+ /**
+ * Get the CDMA NID (Network Identification Number), a number uniquely identifying a network
+ * within a wireless system. (Defined in 3GPP2 C.S0023 3.4.8)
+ * @return The CDMA NID or {@link #UNKNOWN_ID} if not available.
+ */
public int getNetworkId() {
return this.mNetworkId;
}
- /** @hide */
+ /**
+ * Get the CDMA SID (System Identification Number), a number uniquely identifying a wireless
+ * system. (Defined in 3GPP2 C.S0023 3.4.8)
+ * @return The CDMA SID or {@link #UNKNOWN_ID} if not available.
+ */
public int getSystemId() {
return this.mSystemId;
}
@@ -1300,6 +1361,34 @@
return bearerBitmask;
}
+ /** @hide */
+ public static int convertNetworkTypeBitmaskToBearerBitmask(int networkTypeBitmask) {
+ if (networkTypeBitmask == 0) {
+ return 0;
+ }
+ int bearerBitmask = 0;
+ for (int bearerInt = 0; bearerInt < NEXT_RIL_RADIO_TECHNOLOGY; bearerInt++) {
+ if (bitmaskHasTech(networkTypeBitmask, rilRadioTechnologyToNetworkType(bearerInt))) {
+ bearerBitmask |= getBitmaskForTech(bearerInt);
+ }
+ }
+ return bearerBitmask;
+ }
+
+ /** @hide */
+ public static int convertBearerBitmaskToNetworkTypeBitmask(int bearerBitmask) {
+ if (bearerBitmask == 0) {
+ return 0;
+ }
+ int networkTypeBitmask = 0;
+ for (int bearerInt = 0; bearerInt < NEXT_RIL_RADIO_TECHNOLOGY; bearerInt++) {
+ if (bitmaskHasTech(bearerBitmask, bearerInt)) {
+ networkTypeBitmask |= getBitmaskForTech(rilRadioTechnologyToNetworkType(bearerInt));
+ }
+ }
+ return networkTypeBitmask;
+ }
+
/**
* Returns a merged ServiceState consisting of the base SS with voice settings from the
* voice SS. The voice SS is only used if it is IN_SERVICE (otherwise the base SS is returned).
@@ -1318,4 +1407,52 @@
return newSs;
}
+
+ /**
+ * Get all of the available network registration states.
+ *
+ * @return List of registration states
+ * @hide
+ */
+ @SystemApi
+ public List<NetworkRegistrationState> getNetworkRegistrationStates() {
+ return mNetworkRegistrationStates;
+ }
+
+ /**
+ * Get the network registration states with given transport type.
+ *
+ * @param transportType The transport type. See {@link AccessNetworkConstants.TransportType}
+ * @return List of registration states.
+ * @hide
+ */
+ @SystemApi
+ public List<NetworkRegistrationState> getNetworkRegistrationStates(int transportType) {
+ List<NetworkRegistrationState> list = new ArrayList<>();
+ for (NetworkRegistrationState networkRegistrationState : mNetworkRegistrationStates) {
+ if (networkRegistrationState.getTransportType() == transportType) {
+ list.add(networkRegistrationState);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Get the network registration states with given transport type and domain.
+ *
+ * @param transportType The transport type. See {@link AccessNetworkConstants.TransportType}
+ * @param domain The network domain. Must be DOMAIN_CS or DOMAIN_PS.
+ * @return The matching NetworkRegistrationState.
+ * @hide
+ */
+ @SystemApi
+ public NetworkRegistrationState getNetworkRegistrationStates(int transportType, int domain) {
+ for (NetworkRegistrationState networkRegistrationState : mNetworkRegistrationStates) {
+ if (networkRegistrationState.getTransportType() == transportType
+ && networkRegistrationState.getDomain() == domain) {
+ return networkRegistrationState;
+ }
+ }
+ return null;
+ }
}
diff --git a/android/telephony/SignalStrength.java b/android/telephony/SignalStrength.java
index de02de7..fc2ef27 100644
--- a/android/telephony/SignalStrength.java
+++ b/android/telephony/SignalStrength.java
@@ -19,9 +19,13 @@
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
+import android.telephony.CarrierConfigManager;
import android.util.Log;
import android.content.res.Resources;
+import java.util.ArrayList;
+import java.util.Arrays;
+
/**
* Contains phone signal strength related information.
*/
@@ -47,10 +51,15 @@
"none", "poor", "moderate", "good", "great"
};
- /** @hide */
- //Use int max, as -1 is a valid value in signal strength
- public static final int INVALID = 0x7FFFFFFF;
+ /**
+ * Use Integer.MAX_VALUE because -1 is a valid value in signal strength.
+ * @hide
+ */
+ public static final int INVALID = Integer.MAX_VALUE;
+ private static final int LTE_RSRP_THRESHOLDS_NUM = 6;
+
+ /** Parameters reported by the Radio */
private int mGsmSignalStrength; // Valid values are (0-31, 99) as defined in TS 27.007 8.5
private int mGsmBitErrorRate; // bit error rate (0-7, 99) as defined in TS 27.007 8.5
private int mCdmaDbm; // This value is the RSSI value
@@ -63,13 +72,18 @@
private int mLteRsrq;
private int mLteRssnr;
private int mLteCqi;
- private int mLteRsrpBoost; // offset to be reduced from the rsrp threshold while calculating
- // signal strength level
private int mTdScdmaRscp;
- private boolean isGsm; // This value is set by the ServiceStateTracker onSignalStrengthResult
+ /** Parameters from the framework */
+ private int mLteRsrpBoost; // offset to be reduced from the rsrp threshold while calculating
+ // signal strength level
+ private boolean mIsGsm; // This value is set by the ServiceStateTracker
+ // onSignalStrengthResult.
private boolean mUseOnlyRsrpForLteLevel; // Use only RSRP for the number of LTE signal bar.
+ // The threshold of LTE RSRP for determining the display level of LTE signal bar.
+ private int mLteRsrpThresholds[] = new int[LTE_RSRP_THRESHOLDS_NUM];
+
/**
* Create a new SignalStrength from a intent notifier Bundle
*
@@ -94,27 +108,12 @@
* @hide
*/
public SignalStrength() {
- mGsmSignalStrength = 99;
- mGsmBitErrorRate = -1;
- mCdmaDbm = -1;
- mCdmaEcio = -1;
- mEvdoDbm = -1;
- mEvdoEcio = -1;
- mEvdoSnr = -1;
- mLteSignalStrength = 99;
- mLteRsrp = INVALID;
- mLteRsrq = INVALID;
- mLteRssnr = INVALID;
- mLteCqi = INVALID;
- mLteRsrpBoost = 0;
- mTdScdmaRscp = INVALID;
- isGsm = true;
- mUseOnlyRsrpForLteLevel = false;
+ this(true);
}
/**
* This constructor is used to create SignalStrength with default
- * values and set the isGsmFlag with the value passed in the input
+ * values and set the gsmFlag with the value passed in the input
*
* @param gsmFlag true if Gsm Phone,false if Cdma phone
* @return newly created SignalStrength
@@ -133,133 +132,26 @@
mLteRsrq = INVALID;
mLteRssnr = INVALID;
mLteCqi = INVALID;
- mLteRsrpBoost = 0;
mTdScdmaRscp = INVALID;
- isGsm = gsmFlag;
+ mLteRsrpBoost = 0;
+ mIsGsm = gsmFlag;
mUseOnlyRsrpForLteLevel = false;
+ setLteRsrpThresholds(getDefaultLteRsrpThresholds());
}
/**
- * Constructor
+ * Constructor with all fields present
*
* @hide
*/
- public SignalStrength(int gsmSignalStrength, int gsmBitErrorRate,
+ public SignalStrength(
+ int gsmSignalStrength, int gsmBitErrorRate,
int cdmaDbm, int cdmaEcio,
int evdoDbm, int evdoEcio, int evdoSnr,
int lteSignalStrength, int lteRsrp, int lteRsrq, int lteRssnr, int lteCqi,
- int lteRsrpBoost, int tdScdmaRscp, boolean gsmFlag, boolean lteLevelBaseOnRsrp) {
- initialize(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio,
- evdoDbm, evdoEcio, evdoSnr, lteSignalStrength, lteRsrp,
- lteRsrq, lteRssnr, lteCqi, lteRsrpBoost, gsmFlag, lteLevelBaseOnRsrp);
- mTdScdmaRscp = tdScdmaRscp;
- }
-
- /**
- * Constructor
- *
- * @hide
- */
- public SignalStrength(int gsmSignalStrength, int gsmBitErrorRate,
- int cdmaDbm, int cdmaEcio,
- int evdoDbm, int evdoEcio, int evdoSnr,
- int lteSignalStrength, int lteRsrp, int lteRsrq, int lteRssnr, int lteCqi,
- int tdScdmaRscp, boolean gsmFlag) {
- initialize(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio,
- evdoDbm, evdoEcio, evdoSnr, lteSignalStrength, lteRsrp,
- lteRsrq, lteRssnr, lteCqi, 0, gsmFlag, false);
- mTdScdmaRscp = tdScdmaRscp;
- }
-
- /**
- * Constructor
- *
- * @hide
- */
- public SignalStrength(int gsmSignalStrength, int gsmBitErrorRate,
- int cdmaDbm, int cdmaEcio,
- int evdoDbm, int evdoEcio, int evdoSnr,
- int lteSignalStrength, int lteRsrp, int lteRsrq, int lteRssnr, int lteCqi,
- boolean gsmFlag) {
- initialize(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio,
- evdoDbm, evdoEcio, evdoSnr, lteSignalStrength, lteRsrp,
- lteRsrq, lteRssnr, lteCqi, 0, gsmFlag, false);
- }
-
- /**
- * Constructor
- *
- * @hide
- */
- public SignalStrength(int gsmSignalStrength, int gsmBitErrorRate,
- int cdmaDbm, int cdmaEcio,
- int evdoDbm, int evdoEcio, int evdoSnr,
- boolean gsmFlag) {
- initialize(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio,
- evdoDbm, evdoEcio, evdoSnr, 99, INVALID,
- INVALID, INVALID, INVALID, 0, gsmFlag, false);
- }
-
- /**
- * Copy constructors
- *
- * @param s Source SignalStrength
- *
- * @hide
- */
- public SignalStrength(SignalStrength s) {
- copyFrom(s);
- }
-
- /**
- * Initialize gsm/cdma values, sets lte values to defaults.
- *
- * @param gsmSignalStrength
- * @param gsmBitErrorRate
- * @param cdmaDbm
- * @param cdmaEcio
- * @param evdoDbm
- * @param evdoEcio
- * @param evdoSnr
- * @param gsm
- *
- * @hide
- */
- public void initialize(int gsmSignalStrength, int gsmBitErrorRate,
- int cdmaDbm, int cdmaEcio,
- int evdoDbm, int evdoEcio, int evdoSnr,
- boolean gsm) {
- initialize(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio,
- evdoDbm, evdoEcio, evdoSnr, 99, INVALID,
- INVALID, INVALID, INVALID, 0, gsm, false);
- }
-
- /**
- * Initialize all the values
- *
- * @param gsmSignalStrength
- * @param gsmBitErrorRate
- * @param cdmaDbm
- * @param cdmaEcio
- * @param evdoDbm
- * @param evdoEcio
- * @param evdoSnr
- * @param lteSignalStrength
- * @param lteRsrp
- * @param lteRsrq
- * @param lteRssnr
- * @param lteCqi
- * @param lteRsrpBoost
- * @param gsm
- * @param useOnlyRsrpForLteLevel
- *
- * @hide
- */
- public void initialize(int gsmSignalStrength, int gsmBitErrorRate,
- int cdmaDbm, int cdmaEcio,
- int evdoDbm, int evdoEcio, int evdoSnr,
- int lteSignalStrength, int lteRsrp, int lteRsrq, int lteRssnr, int lteCqi,
- int lteRsrpBoost, boolean gsm, boolean useOnlyRsrpForLteLevel) {
+ int tdScdmaRscp,
+ // values Added by config
+ int lteRsrpBoost, boolean gsmFlag, boolean lteLevelBaseOnRsrp) {
mGsmSignalStrength = gsmSignalStrength;
mGsmBitErrorRate = gsmBitErrorRate;
mCdmaDbm = cdmaDbm;
@@ -272,14 +164,41 @@
mLteRsrq = lteRsrq;
mLteRssnr = lteRssnr;
mLteCqi = lteCqi;
- mLteRsrpBoost = lteRsrpBoost;
mTdScdmaRscp = INVALID;
- isGsm = gsm;
- mUseOnlyRsrpForLteLevel = useOnlyRsrpForLteLevel;
+ mLteRsrpBoost = lteRsrpBoost;
+ mIsGsm = gsmFlag;
+ mUseOnlyRsrpForLteLevel = lteLevelBaseOnRsrp;
+ setLteRsrpThresholds(getDefaultLteRsrpThresholds());
if (DBG) log("initialize: " + toString());
}
/**
+ * Constructor for only values provided by Radio HAL
+ *
+ * @hide
+ */
+ public SignalStrength(int gsmSignalStrength, int gsmBitErrorRate,
+ int cdmaDbm, int cdmaEcio,
+ int evdoDbm, int evdoEcio, int evdoSnr,
+ int lteSignalStrength, int lteRsrp, int lteRsrq, int lteRssnr, int lteCqi,
+ int tdScdmaRscp) {
+ this(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio,
+ evdoDbm, evdoEcio, evdoSnr, lteSignalStrength, lteRsrp,
+ lteRsrq, lteRssnr, lteCqi, tdScdmaRscp, 0, true, false);
+ }
+
+ /**
+ * Copy constructors
+ *
+ * @param s Source SignalStrength
+ *
+ * @hide
+ */
+ public SignalStrength(SignalStrength s) {
+ copyFrom(s);
+ }
+
+ /**
* @hide
*/
protected void copyFrom(SignalStrength s) {
@@ -295,10 +214,11 @@
mLteRsrq = s.mLteRsrq;
mLteRssnr = s.mLteRssnr;
mLteCqi = s.mLteCqi;
- mLteRsrpBoost = s.mLteRsrpBoost;
mTdScdmaRscp = s.mTdScdmaRscp;
- isGsm = s.isGsm;
+ mLteRsrpBoost = s.mLteRsrpBoost;
+ mIsGsm = s.mIsGsm;
mUseOnlyRsrpForLteLevel = s.mUseOnlyRsrpForLteLevel;
+ setLteRsrpThresholds(s.mLteRsrpThresholds);
}
/**
@@ -321,37 +241,11 @@
mLteRsrq = in.readInt();
mLteRssnr = in.readInt();
mLteCqi = in.readInt();
- mLteRsrpBoost = in.readInt();
mTdScdmaRscp = in.readInt();
- isGsm = (in.readInt() != 0);
- mUseOnlyRsrpForLteLevel = (in.readInt() != 0);
- }
-
- /**
- * Make a SignalStrength object from the given parcel as passed up by
- * the ril which does not have isGsm. isGsm will be changed by ServiceStateTracker
- * so the default is a don't care.
- *
- * @hide
- */
- public static SignalStrength makeSignalStrengthFromRilParcel(Parcel in) {
- if (DBG) log("Size of signalstrength parcel:" + in.dataSize());
-
- SignalStrength ss = new SignalStrength();
- ss.mGsmSignalStrength = in.readInt();
- ss.mGsmBitErrorRate = in.readInt();
- ss.mCdmaDbm = in.readInt();
- ss.mCdmaEcio = in.readInt();
- ss.mEvdoDbm = in.readInt();
- ss.mEvdoEcio = in.readInt();
- ss.mEvdoSnr = in.readInt();
- ss.mLteSignalStrength = in.readInt();
- ss.mLteRsrp = in.readInt();
- ss.mLteRsrq = in.readInt();
- ss.mLteRssnr = in.readInt();
- ss.mLteCqi = in.readInt();
- ss.mTdScdmaRscp = in.readInt();
- return ss;
+ mLteRsrpBoost = in.readInt();
+ mIsGsm = in.readBoolean();
+ mUseOnlyRsrpForLteLevel = in.readBoolean();
+ in.readIntArray(mLteRsrpThresholds);
}
/**
@@ -370,10 +264,11 @@
out.writeInt(mLteRsrq);
out.writeInt(mLteRssnr);
out.writeInt(mLteCqi);
- out.writeInt(mLteRsrpBoost);
out.writeInt(mTdScdmaRscp);
- out.writeInt(isGsm ? 1 : 0);
- out.writeInt(mUseOnlyRsrpForLteLevel ? 1 : 0);
+ out.writeInt(mLteRsrpBoost);
+ out.writeBoolean(mIsGsm);
+ out.writeBoolean(mUseOnlyRsrpForLteLevel);
+ out.writeIntArray(mLteRsrpThresholds);
}
/**
@@ -436,24 +331,24 @@
}
/**
- * Fix {@link #isGsm} based on the signal strength data.
+ * Fix {@link #mIsGsm} based on the signal strength data.
*
* @hide
*/
public void fixType() {
- isGsm = getCdmaRelatedSignalStrength() == SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
+ mIsGsm = getCdmaRelatedSignalStrength() == SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
}
/**
* @param true - Gsm, Lte phones
* false - Cdma phones
*
- * Used by voice phone to set the isGsm
+ * Used by voice phone to set the mIsGsm
* flag
* @hide
*/
public void setGsm(boolean gsmFlag) {
- isGsm = gsmFlag;
+ mIsGsm = gsmFlag;
}
/**
@@ -480,6 +375,22 @@
}
/**
+ * Sets the threshold array for determining the display level of LTE signal bar.
+ *
+ * @param lteRsrpThresholds int array for determining the display level.
+ *
+ * @hide
+ */
+ public void setLteRsrpThresholds(int[] lteRsrpThresholds) {
+ if ((lteRsrpThresholds == null)
+ || (lteRsrpThresholds.length != LTE_RSRP_THRESHOLDS_NUM)) {
+ Log.wtf(LOG_TAG, "setLteRsrpThresholds - lteRsrpThresholds is invalid.");
+ return;
+ }
+ System.arraycopy(lteRsrpThresholds, 0, mLteRsrpThresholds, 0, LTE_RSRP_THRESHOLDS_NUM);
+ }
+
+ /**
* Get the GSM Signal Strength, valid values are (0-31, 99) as defined in TS
* 27.007 8.5
*/
@@ -568,7 +479,7 @@
* while 4 represents a very strong signal strength.
*/
public int getLevel() {
- int level = isGsm ? getGsmRelatedSignalStrength() : getCdmaRelatedSignalStrength();
+ int level = mIsGsm ? getGsmRelatedSignalStrength() : getCdmaRelatedSignalStrength();
if (DBG) log("getLevel=" + level);
return level;
}
@@ -580,15 +491,13 @@
*/
public int getAsuLevel() {
int asuLevel = 0;
- if (isGsm) {
- if (getLteLevel() == SIGNAL_STRENGTH_NONE_OR_UNKNOWN) {
- if (getTdScdmaLevel() == SIGNAL_STRENGTH_NONE_OR_UNKNOWN) {
- asuLevel = getGsmAsuLevel();
- } else {
- asuLevel = getTdScdmaAsuLevel();
- }
- } else {
+ if (mIsGsm) {
+ if (mLteRsrp != SignalStrength.INVALID) {
asuLevel = getLteAsuLevel();
+ } else if (mTdScdmaRscp != SignalStrength.INVALID) {
+ asuLevel = getTdScdmaAsuLevel();
+ } else {
+ asuLevel = getGsmAsuLevel();
}
} else {
int cdmaAsuLevel = getCdmaAsuLevel();
@@ -833,25 +742,18 @@
*/
int rssiIconLevel = SIGNAL_STRENGTH_NONE_OR_UNKNOWN, rsrpIconLevel = -1, snrIconLevel = -1;
- int[] threshRsrp = Resources.getSystem().getIntArray(
- com.android.internal.R.array.config_lteDbmThresholds);
- if (threshRsrp.length != 6) {
- Log.wtf(LOG_TAG, "getLteLevel - config_lteDbmThresholds has invalid num of elements."
- + " Cannot evaluate RSRP signal.");
- } else {
- if (mLteRsrp > threshRsrp[5]) {
- rsrpIconLevel = -1;
- } else if (mLteRsrp >= (threshRsrp[4] - mLteRsrpBoost)) {
- rsrpIconLevel = SIGNAL_STRENGTH_GREAT;
- } else if (mLteRsrp >= (threshRsrp[3] - mLteRsrpBoost)) {
- rsrpIconLevel = SIGNAL_STRENGTH_GOOD;
- } else if (mLteRsrp >= (threshRsrp[2] - mLteRsrpBoost)) {
- rsrpIconLevel = SIGNAL_STRENGTH_MODERATE;
- } else if (mLteRsrp >= (threshRsrp[1] - mLteRsrpBoost)) {
- rsrpIconLevel = SIGNAL_STRENGTH_POOR;
- } else if (mLteRsrp >= threshRsrp[0]) {
- rsrpIconLevel = SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
- }
+ if (mLteRsrp > mLteRsrpThresholds[5]) {
+ rsrpIconLevel = -1;
+ } else if (mLteRsrp >= (mLteRsrpThresholds[4] - mLteRsrpBoost)) {
+ rsrpIconLevel = SIGNAL_STRENGTH_GREAT;
+ } else if (mLteRsrp >= (mLteRsrpThresholds[3] - mLteRsrpBoost)) {
+ rsrpIconLevel = SIGNAL_STRENGTH_GOOD;
+ } else if (mLteRsrp >= (mLteRsrpThresholds[2] - mLteRsrpBoost)) {
+ rsrpIconLevel = SIGNAL_STRENGTH_MODERATE;
+ } else if (mLteRsrp >= (mLteRsrpThresholds[1] - mLteRsrpBoost)) {
+ rsrpIconLevel = SIGNAL_STRENGTH_POOR;
+ } else if (mLteRsrp >= mLteRsrpThresholds[0]) {
+ rsrpIconLevel = SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
}
if (useOnlyRsrpForLteLevel()) {
@@ -937,7 +839,7 @@
* @return true if this is for GSM
*/
public boolean isGsm() {
- return this.isGsm;
+ return this.mIsGsm;
}
/**
@@ -1009,8 +911,8 @@
+ (mEvdoDbm * primeNum) + (mEvdoEcio * primeNum) + (mEvdoSnr * primeNum)
+ (mLteSignalStrength * primeNum) + (mLteRsrp * primeNum)
+ (mLteRsrq * primeNum) + (mLteRssnr * primeNum) + (mLteCqi * primeNum)
- + (mLteRsrpBoost * primeNum) + (mTdScdmaRscp * primeNum) + (isGsm ? 1 : 0)
- + (mUseOnlyRsrpForLteLevel ? 1 : 0));
+ + (mLteRsrpBoost * primeNum) + (mTdScdmaRscp * primeNum) + (mIsGsm ? 1 : 0)
+ + (mUseOnlyRsrpForLteLevel ? 1 : 0) + (Arrays.hashCode(mLteRsrpThresholds)));
}
/**
@@ -1044,8 +946,9 @@
&& mLteCqi == s.mLteCqi
&& mLteRsrpBoost == s.mLteRsrpBoost
&& mTdScdmaRscp == s.mTdScdmaRscp
- && isGsm == s.isGsm
- && mUseOnlyRsrpForLteLevel == s.mUseOnlyRsrpForLteLevel);
+ && mIsGsm == s.mIsGsm
+ && mUseOnlyRsrpForLteLevel == s.mUseOnlyRsrpForLteLevel
+ && Arrays.equals(mLteRsrpThresholds, s.mLteRsrpThresholds));
}
/**
@@ -1068,9 +971,10 @@
+ " " + mLteCqi
+ " " + mLteRsrpBoost
+ " " + mTdScdmaRscp
- + " " + (isGsm ? "gsm|lte" : "cdma")
+ + " " + (mIsGsm ? "gsm|lte" : "cdma")
+ " " + (mUseOnlyRsrpForLteLevel ? "use_only_rsrp_for_lte_level" :
- "use_rsrp_and_rssnr_for_lte_level"));
+ "use_rsrp_and_rssnr_for_lte_level")
+ + " " + (Arrays.toString(mLteRsrpThresholds)));
}
/** Returns the signal strength related to GSM. */
@@ -1122,10 +1026,14 @@
mLteRsrq = m.getInt("LteRsrq");
mLteRssnr = m.getInt("LteRssnr");
mLteCqi = m.getInt("LteCqi");
- mLteRsrpBoost = m.getInt("lteRsrpBoost");
+ mLteRsrpBoost = m.getInt("LteRsrpBoost");
mTdScdmaRscp = m.getInt("TdScdma");
- isGsm = m.getBoolean("isGsm");
- mUseOnlyRsrpForLteLevel = m.getBoolean("useOnlyRsrpForLteLevel");
+ mIsGsm = m.getBoolean("IsGsm");
+ mUseOnlyRsrpForLteLevel = m.getBoolean("UseOnlyRsrpForLteLevel");
+ ArrayList<Integer> lteRsrpThresholds = m.getIntegerArrayList("lteRsrpThresholds");
+ for (int i = 0; i < lteRsrpThresholds.size(); i++) {
+ mLteRsrpThresholds[i] = lteRsrpThresholds.get(i);
+ }
}
/**
@@ -1147,10 +1055,25 @@
m.putInt("LteRsrq", mLteRsrq);
m.putInt("LteRssnr", mLteRssnr);
m.putInt("LteCqi", mLteCqi);
- m.putInt("lteRsrpBoost", mLteRsrpBoost);
+ m.putInt("LteRsrpBoost", mLteRsrpBoost);
m.putInt("TdScdma", mTdScdmaRscp);
- m.putBoolean("isGsm", isGsm);
- m.putBoolean("useOnlyRsrpForLteLevel", mUseOnlyRsrpForLteLevel);
+ m.putBoolean("IsGsm", mIsGsm);
+ m.putBoolean("UseOnlyRsrpForLteLevel", mUseOnlyRsrpForLteLevel);
+ ArrayList<Integer> lteRsrpThresholds = new ArrayList<Integer>();
+ for (int value : mLteRsrpThresholds) {
+ lteRsrpThresholds.add(value);
+ }
+ m.putIntegerArrayList("lteRsrpThresholds", lteRsrpThresholds);
+ }
+
+ /**
+ * Gets the default threshold array for determining the display level of LTE signal bar.
+ *
+ * @return int array for determining the display level.
+ */
+ private int[] getDefaultLteRsrpThresholds() {
+ return CarrierConfigManager.getDefaultConfig().getIntArray(
+ CarrierConfigManager.KEY_LTE_RSRP_THRESHOLDS_INT_ARRAY);
}
/**
diff --git a/android/telephony/SubscriptionInfo.java b/android/telephony/SubscriptionInfo.java
index 4e1c15f..38408fe 100644
--- a/android/telephony/SubscriptionInfo.java
+++ b/android/telephony/SubscriptionInfo.java
@@ -126,14 +126,31 @@
private UiccAccessRule[] mAccessRules;
/**
+ * The ID of the SIM card. It is the ICCID of the active profile for a UICC card and the EID
+ * for an eUICC card.
+ */
+ private String mCardId;
+
+ /**
+ * @hide
+ */
+ public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName,
+ CharSequence carrierName, int nameSource, int iconTint, String number, int roaming,
+ Bitmap icon, int mcc, int mnc, String countryIso) {
+ this(id, iccId, simSlotIndex, displayName, carrierName, nameSource, iconTint, number,
+ roaming, icon, mcc, mnc, countryIso, false /* isEmbedded */,
+ null /* accessRules */, null /* accessRules */);
+ }
+
+ /**
* @hide
*/
public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName,
CharSequence carrierName, int nameSource, int iconTint, String number, int roaming,
- Bitmap icon, int mcc, int mnc, String countryIso) {
+ Bitmap icon, int mcc, int mnc, String countryIso, boolean isEmbedded,
+ @Nullable UiccAccessRule[] accessRules) {
this(id, iccId, simSlotIndex, displayName, carrierName, nameSource, iconTint, number,
- roaming, icon, mcc, mnc, countryIso, false /* isEmbedded */,
- null /* accessRules */);
+ roaming, icon, mcc, mnc, countryIso, isEmbedded, accessRules, null /* cardId */);
}
/**
@@ -142,7 +159,7 @@
public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName,
CharSequence carrierName, int nameSource, int iconTint, String number, int roaming,
Bitmap icon, int mcc, int mnc, String countryIso, boolean isEmbedded,
- @Nullable UiccAccessRule[] accessRules) {
+ @Nullable UiccAccessRule[] accessRules, String cardId) {
this.mId = id;
this.mIccId = iccId;
this.mSimSlotIndex = simSlotIndex;
@@ -158,6 +175,7 @@
this.mCountryIso = countryIso;
this.mIsEmbedded = isEmbedded;
this.mAccessRules = accessRules;
+ this.mCardId = cardId;
}
/**
@@ -387,6 +405,14 @@
return mAccessRules;
}
+ /**
+ * @return the ID of the SIM card which contains the subscription.
+ * @hide
+ */
+ public String getCardId() {
+ return this.mCardId;
+ }
+
public static final Parcelable.Creator<SubscriptionInfo> CREATOR = new Parcelable.Creator<SubscriptionInfo>() {
@Override
public SubscriptionInfo createFromParcel(Parcel source) {
@@ -405,10 +431,11 @@
Bitmap iconBitmap = Bitmap.CREATOR.createFromParcel(source);
boolean isEmbedded = source.readBoolean();
UiccAccessRule[] accessRules = source.createTypedArray(UiccAccessRule.CREATOR);
+ String cardId = source.readString();
return new SubscriptionInfo(id, iccId, simSlotIndex, displayName, carrierName,
nameSource, iconTint, number, dataRoaming, iconBitmap, mcc, mnc, countryIso,
- isEmbedded, accessRules);
+ isEmbedded, accessRules, cardId);
}
@Override
@@ -434,6 +461,7 @@
mIconBitmap.writeToParcel(dest, flags);
dest.writeBoolean(mIsEmbedded);
dest.writeTypedArray(mAccessRules, flags);
+ dest.writeString(mCardId);
}
@Override
@@ -459,11 +487,13 @@
@Override
public String toString() {
String iccIdToPrint = givePrintableIccid(mIccId);
+ String cardIdToPrint = givePrintableIccid(mCardId);
return "{id=" + mId + ", iccId=" + iccIdToPrint + " simSlotIndex=" + mSimSlotIndex
+ " displayName=" + mDisplayName + " carrierName=" + mCarrierName
+ " nameSource=" + mNameSource + " iconTint=" + mIconTint
+ " dataRoaming=" + mDataRoaming + " iconBitmap=" + mIconBitmap + " mcc " + mMcc
+ " mnc " + mMnc + " isEmbedded " + mIsEmbedded
- + " accessRules " + Arrays.toString(mAccessRules) + "}";
+ + " accessRules " + Arrays.toString(mAccessRules)
+ + " cardId=" + cardIdToPrint + "}";
}
}
diff --git a/android/telephony/SubscriptionManager.java b/android/telephony/SubscriptionManager.java
index 1e6abf2..debf43d 100644
--- a/android/telephony/SubscriptionManager.java
+++ b/android/telephony/SubscriptionManager.java
@@ -16,31 +16,44 @@
package android.telephony;
+import static android.net.NetworkPolicyManager.OVERRIDE_CONGESTED;
+import static android.net.NetworkPolicyManager.OVERRIDE_UNMETERED;
+
+import android.annotation.DurationMillisLong;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
-import android.annotation.SystemApi;
import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.app.BroadcastOptions;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.net.INetworkPolicyManager;
+import android.net.NetworkCapabilities;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.ServiceManager.ServiceNotFoundException;
import android.util.DisplayMetrics;
+
import com.android.internal.telephony.IOnSubscriptionsChangedListener;
import com.android.internal.telephony.ISub;
import com.android.internal.telephony.ITelephonyRegistry;
import com.android.internal.telephony.PhoneConstants;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.TimeUnit;
/**
* SubscriptionManager is the application interface to SubscriptionController
@@ -271,6 +284,14 @@
public static final String IS_EMBEDDED = "is_embedded";
/**
+ * TelephonyProvider column name for SIM card identifier. For UICC card it is the ICCID of the
+ * current enabled profile on the card, while for eUICC card it is the EID of the card.
+ * <P>Type: TEXT (String)</P>
+ * @hide
+ */
+ public static final String CARD_ID = "card_id";
+
+ /**
* TelephonyProvider column name for the encoded {@link UiccAccessRule}s from
* {@link UiccAccessRule#encodeRules}. Only present if {@link #IS_EMBEDDED} is 1.
* <p>TYPE: BLOB
@@ -430,6 +451,55 @@
= "android.telephony.action.DEFAULT_SMS_SUBSCRIPTION_CHANGED";
/**
+ * Activity Action: Display UI for managing the billing relationship plans
+ * between a carrier and a specific subscriber.
+ * <p>
+ * Carrier apps are encouraged to implement this activity, and the OS will
+ * provide an affordance to quickly enter this activity, typically via
+ * Settings. This affordance will only be shown when the carrier app is
+ * actively providing subscription plan information via
+ * {@link #setSubscriptionPlans(int, List)}.
+ * <p>
+ * Contains {@link #EXTRA_SUBSCRIPTION_INDEX} to indicate which subscription
+ * the user is interested in.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ @SystemApi
+ public static final String ACTION_MANAGE_SUBSCRIPTION_PLANS
+ = "android.telephony.action.MANAGE_SUBSCRIPTION_PLANS";
+
+ /**
+ * Broadcast Action: Request a refresh of the billing relationship plans
+ * between a carrier and a specific subscriber.
+ * <p>
+ * Carrier apps are encouraged to implement this receiver, and the OS will
+ * provide an affordance to request a refresh. This affordance will only be
+ * shown when the carrier app is actively providing subscription plan
+ * information via {@link #setSubscriptionPlans(int, List)}.
+ * <p>
+ * Contains {@link #EXTRA_SUBSCRIPTION_INDEX} to indicate which subscription
+ * the user is interested in.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SystemApi
+ public static final String ACTION_REFRESH_SUBSCRIPTION_PLANS
+ = "android.telephony.action.REFRESH_SUBSCRIPTION_PLANS";
+
+ /**
+ * Broadcast Action: The billing relationship plans between a carrier and a
+ * specific subscriber has changed.
+ * <p>
+ * Contains {@link #EXTRA_SUBSCRIPTION_INDEX} to indicate which subscription
+ * changed.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_PLANS)
+ public static final String ACTION_SUBSCRIPTION_PLANS_CHANGED
+ = "android.telephony.action.SUBSCRIPTION_PLANS_CHANGED";
+
+ /**
* Integer extra used with {@link #ACTION_DEFAULT_SUBSCRIPTION_CHANGED} and
* {@link #ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED} to indicate the subscription
* which has changed.
@@ -437,6 +507,7 @@
public static final String EXTRA_SUBSCRIPTION_INDEX = "android.telephony.extra.SUBSCRIPTION_INDEX";
private final Context mContext;
+ private INetworkPolicyManager mNetworkPolicy;
/**
* A listener class for monitoring changes to {@link SubscriptionInfo} records.
@@ -515,16 +586,21 @@
}
/**
- * Get an instance of the SubscriptionManager from the Context.
- * This invokes {@link android.content.Context#getSystemService
- * Context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE)}.
- *
- * @param context to use.
- * @return SubscriptionManager instance
+ * @deprecated developers should always obtain references directly from
+ * {@link Context#getSystemService(Class)}.
*/
+ @Deprecated
public static SubscriptionManager from(Context context) {
- return (SubscriptionManager) context.getSystemService(
- Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+ return (SubscriptionManager) context
+ .getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+ }
+
+ private final INetworkPolicyManager getNetworkPolicy() {
+ if (mNetworkPolicy == null) {
+ mNetworkPolicy = INetworkPolicyManager.Stub
+ .asInterface(ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
+ }
+ return mNetworkPolicy;
}
/**
@@ -1612,21 +1688,18 @@
* This method is only accessible to the following narrow set of apps:
* <ul>
* <li>The carrier app for this subscriberId, as determined by
- * {@link TelephonyManager#hasCarrierPrivileges(int)}.
+ * {@link TelephonyManager#hasCarrierPrivileges()}.
* <li>The carrier app explicitly delegated access through
* {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}.
* </ul>
*
* @param subId the subscriber this relationship applies to
- * @hide
*/
@SystemApi
public @NonNull List<SubscriptionPlan> getSubscriptionPlans(int subId) {
- final INetworkPolicyManager npm = INetworkPolicyManager.Stub
- .asInterface(ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
try {
SubscriptionPlan[] subscriptionPlans =
- npm.getSubscriptionPlans(subId, mContext.getOpPackageName());
+ getNetworkPolicy().getSubscriptionPlans(subId, mContext.getOpPackageName());
return subscriptionPlans == null
? Collections.emptyList() : Arrays.asList(subscriptionPlans);
} catch (RemoteException e) {
@@ -1641,7 +1714,7 @@
* This method is only accessible to the following narrow set of apps:
* <ul>
* <li>The carrier app for this subscriberId, as determined by
- * {@link TelephonyManager#hasCarrierPrivileges(int)}.
+ * {@link TelephonyManager#hasCarrierPrivileges()}.
* <li>The carrier app explicitly delegated access through
* {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}.
* </ul>
@@ -1650,17 +1723,173 @@
* @param plans the list of plans. The first plan is always the primary and
* most important plan. Any additional plans are secondary and
* may not be displayed or used by decision making logic.
- * @hide
*/
@SystemApi
public void setSubscriptionPlans(int subId, @NonNull List<SubscriptionPlan> plans) {
- final INetworkPolicyManager npm = INetworkPolicyManager.Stub
- .asInterface(ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
try {
- npm.setSubscriptionPlans(subId, plans.toArray(new SubscriptionPlan[plans.size()]),
- mContext.getOpPackageName());
+ getNetworkPolicy().setSubscriptionPlans(subId,
+ plans.toArray(new SubscriptionPlan[plans.size()]), mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
+
+ /** @hide */
+ private String getSubscriptionPlansOwner(int subId) {
+ try {
+ return getNetworkPolicy().getSubscriptionPlansOwner(subId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Temporarily override the billing relationship plan between a carrier and
+ * a specific subscriber to be considered unmetered. This will be reflected
+ * to apps via {@link NetworkCapabilities#NET_CAPABILITY_NOT_METERED}.
+ * <p>
+ * This method is only accessible to the following narrow set of apps:
+ * <ul>
+ * <li>The carrier app for this subscriberId, as determined by
+ * {@link TelephonyManager#hasCarrierPrivileges()}.
+ * <li>The carrier app explicitly delegated access through
+ * {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}.
+ * </ul>
+ *
+ * @param subId the subscriber this override applies to.
+ * @param overrideUnmetered set if the billing relationship should be
+ * considered unmetered.
+ * @param timeoutMillis the timeout after which the requested override will
+ * be automatically cleared, or {@code 0} to leave in the
+ * requested state until explicitly cleared, or the next reboot,
+ * whichever happens first.
+ */
+ @SystemApi
+ public void setSubscriptionOverrideUnmetered(int subId, boolean overrideUnmetered,
+ @DurationMillisLong long timeoutMillis) {
+ try {
+ final int overrideValue = overrideUnmetered ? OVERRIDE_UNMETERED : 0;
+ mNetworkPolicy.setSubscriptionOverride(subId, OVERRIDE_UNMETERED, overrideValue,
+ timeoutMillis, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Temporarily override the billing relationship plan between a carrier and
+ * a specific subscriber to be considered congested. This will cause the
+ * device to delay certain network requests when possible, such as developer
+ * jobs that are willing to run in a flexible time window.
+ * <p>
+ * This method is only accessible to the following narrow set of apps:
+ * <ul>
+ * <li>The carrier app for this subscriberId, as determined by
+ * {@link TelephonyManager#hasCarrierPrivileges()}.
+ * <li>The carrier app explicitly delegated access through
+ * {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}.
+ * </ul>
+ *
+ * @param subId the subscriber this override applies to.
+ * @param overrideCongested set if the subscription should be considered
+ * congested.
+ * @param timeoutMillis the timeout after which the requested override will
+ * be automatically cleared, or {@code 0} to leave in the
+ * requested state until explicitly cleared, or the next reboot,
+ * whichever happens first.
+ */
+ @SystemApi
+ public void setSubscriptionOverrideCongested(int subId, boolean overrideCongested,
+ @DurationMillisLong long timeoutMillis) {
+ try {
+ final int overrideValue = overrideCongested ? OVERRIDE_CONGESTED : 0;
+ mNetworkPolicy.setSubscriptionOverride(subId, OVERRIDE_CONGESTED, overrideValue,
+ timeoutMillis, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Create an {@link Intent} that can be launched towards the carrier app
+ * that is currently defining the billing relationship plan through
+ * {@link #setSubscriptionPlans(int, List)}.
+ *
+ * @return ready to launch Intent targeted towards the carrier app, or
+ * {@code null} if no carrier app is defined, or if the defined
+ * carrier app provides no management activity.
+ * @hide
+ */
+ public @Nullable Intent createManageSubscriptionIntent(int subId) {
+ // Bail if no owner
+ final String owner = getSubscriptionPlansOwner(subId);
+ if (owner == null) return null;
+
+ // Bail if no plans
+ final List<SubscriptionPlan> plans = getSubscriptionPlans(subId);
+ if (plans.isEmpty()) return null;
+
+ final Intent intent = new Intent(ACTION_MANAGE_SUBSCRIPTION_PLANS);
+ intent.setPackage(owner);
+ intent.putExtra(EXTRA_SUBSCRIPTION_INDEX, subId);
+
+ // Bail if not implemented
+ if (mContext.getPackageManager().queryIntentActivities(intent,
+ PackageManager.MATCH_DEFAULT_ONLY).isEmpty()) {
+ return null;
+ }
+
+ return intent;
+ }
+
+ /** @hide */
+ private @Nullable Intent createRefreshSubscriptionIntent(int subId) {
+ // Bail if no owner
+ final String owner = getSubscriptionPlansOwner(subId);
+ if (owner == null) return null;
+
+ // Bail if no plans
+ final List<SubscriptionPlan> plans = getSubscriptionPlans(subId);
+ if (plans.isEmpty()) return null;
+
+ final Intent intent = new Intent(ACTION_REFRESH_SUBSCRIPTION_PLANS);
+ intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ intent.setPackage(owner);
+ intent.putExtra(EXTRA_SUBSCRIPTION_INDEX, subId);
+
+ // Bail if not implemented
+ if (mContext.getPackageManager().queryBroadcastReceivers(intent, 0).isEmpty()) {
+ return null;
+ }
+
+ return intent;
+ }
+
+ /**
+ * Check if there is a carrier app that is currently defining the billing
+ * relationship plan through {@link #setSubscriptionPlans(int, List)} that
+ * supports refreshing of subscription plans.
+ *
+ * @hide
+ */
+ public boolean isSubscriptionPlansRefreshSupported(int subId) {
+ return createRefreshSubscriptionIntent(subId) != null;
+ }
+
+ /**
+ * Request that the carrier app that is currently defining the billing
+ * relationship plan through {@link #setSubscriptionPlans(int, List)}
+ * refresh its subscription plans.
+ * <p>
+ * If the app is able to successfully update the plans, you'll expect to
+ * receive the {@link #ACTION_SUBSCRIPTION_PLANS_CHANGED} broadcast.
+ *
+ * @hide
+ */
+ public void requestSubscriptionPlansRefresh(int subId) {
+ final Intent intent = createRefreshSubscriptionIntent(subId);
+ final BroadcastOptions options = BroadcastOptions.makeBasic();
+ options.setTemporaryAppWhitelistDuration(TimeUnit.MINUTES.toMillis(1));
+ mContext.sendBroadcast(intent, null, options.toBundle());
+ }
}
diff --git a/android/telephony/SubscriptionPlan.java b/android/telephony/SubscriptionPlan.java
index 265e3e7..9411652 100644
--- a/android/telephony/SubscriptionPlan.java
+++ b/android/telephony/SubscriptionPlan.java
@@ -43,7 +43,6 @@
*
* @see SubscriptionManager#setSubscriptionPlans(int, java.util.List)
* @see SubscriptionManager#getSubscriptionPlans(int)
- * @hide
*/
@SystemApi
public final class SubscriptionPlan implements Parcelable {
diff --git a/android/telephony/TelephonyManager.java b/android/telephony/TelephonyManager.java
index af5b190..0a6d960 100644
--- a/android/telephony/TelephonyManager.java
+++ b/android/telephony/TelephonyManager.java
@@ -22,8 +22,8 @@
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
-import android.annotation.SuppressLint;
import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.WorkerThread;
@@ -53,6 +53,7 @@
import com.android.ims.internal.IImsMMTelFeature;
import com.android.ims.internal.IImsRcsFeature;
+import com.android.ims.internal.IImsRegistration;
import com.android.ims.internal.IImsServiceFeatureCallback;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telecom.ITelecomService;
@@ -60,7 +61,6 @@
import com.android.internal.telephony.IPhoneSubInfo;
import com.android.internal.telephony.ITelephony;
import com.android.internal.telephony.ITelephonyRegistry;
-import com.android.internal.telephony.OperatorInfo;
import com.android.internal.telephony.PhoneConstants;
import com.android.internal.telephony.RILConstants;
import com.android.internal.telephony.TelephonyProperties;
@@ -831,6 +831,17 @@
"android.telephony.event.EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE";
/**
+ * {@link android.telecom.Connection} event used to indicate that an IMS call has be
+ * successfully handed over from LTE to WIFI.
+ * <p>
+ * Sent via {@link android.telecom.Connection#sendConnectionEvent(String, Bundle)}.
+ * The {@link Bundle} parameter is expected to be null when this connection event is used.
+ * @hide
+ */
+ public static final String EVENT_HANDOVER_VIDEO_FROM_LTE_TO_WIFI =
+ "android.telephony.event.EVENT_HANDOVER_VIDEO_FROM_LTE_TO_WIFI";
+
+ /**
* {@link android.telecom.Connection} event used to indicate that an IMS call failed to be
* handed over from LTE to WIFI.
* <p>
@@ -1012,8 +1023,8 @@
/**
* An int extra used with {@link #ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED} which indicates
- * the updated carrier id {@link TelephonyManager#getSubscriptionCarrierId()} of the current
- * subscription.
+ * the updated carrier id {@link TelephonyManager#getAndroidCarrierIdForSubscription()} of
+ * the current subscription.
* <p>Will be {@link TelephonyManager#UNKNOWN_CARRIER_ID} if the subscription is unavailable or
* the carrier cannot be identified.
*/
@@ -2110,6 +2121,110 @@
* carrier restrictions.
*/
public static final int SIM_STATE_CARD_RESTRICTED = 9;
+ /**
+ * SIM card state: Loaded: SIM card applications have been loaded
+ * @hide
+ */
+ @SystemApi
+ public static final int SIM_STATE_LOADED = 10;
+ /**
+ * SIM card state: SIM Card is present
+ * @hide
+ */
+ @SystemApi
+ public static final int SIM_STATE_PRESENT = 11;
+
+ /**
+ * Extra included in {@link #ACTION_SIM_CARD_STATE_CHANGED} and
+ * {@link #ACTION_SIM_APPLICATION_STATE_CHANGED} to indicate the card/application state.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String EXTRA_SIM_STATE = "android.telephony.extra.SIM_STATE";
+
+ /**
+ * Broadcast Action: The sim card state has changed.
+ * The intent will have the following extra values:</p>
+ * <dl>
+ * <dt>{@link #EXTRA_SIM_STATE}</dt>
+ * <dd>The sim card state. One of:
+ * <dl>
+ * <dt>{@link #SIM_STATE_ABSENT}</dt>
+ * <dd>SIM card not found</dd>
+ * <dt>{@link #SIM_STATE_CARD_IO_ERROR}</dt>
+ * <dd>SIM card IO error</dd>
+ * <dt>{@link #SIM_STATE_CARD_RESTRICTED}</dt>
+ * <dd>SIM card is restricted</dd>
+ * <dt>{@link #SIM_STATE_PRESENT}</dt>
+ * <dd>SIM card is present</dd>
+ * </dl>
+ * </dd>
+ * </dl>
+ *
+ * <p class="note">Requires the READ_PRIVILEGED_PHONE_STATE permission.
+ *
+ * <p class="note">The current state can also be queried using {@link #getSimCardState()}.
+ *
+ * <p class="note">This is a protected intent that can only be sent by the system.
+ * @hide
+ */
+ @SystemApi
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_SIM_CARD_STATE_CHANGED =
+ "android.telephony.action.SIM_CARD_STATE_CHANGED";
+
+ /**
+ * Broadcast Action: The sim application state has changed.
+ * The intent will have the following extra values:</p>
+ * <dl>
+ * <dt>{@link #EXTRA_SIM_STATE}</dt>
+ * <dd>The sim application state. One of:
+ * <dl>
+ * <dt>{@link #SIM_STATE_NOT_READY}</dt>
+ * <dd>SIM card applications not ready</dd>
+ * <dt>{@link #SIM_STATE_PIN_REQUIRED}</dt>
+ * <dd>SIM card PIN locked</dd>
+ * <dt>{@link #SIM_STATE_PUK_REQUIRED}</dt>
+ * <dd>SIM card PUK locked</dd>
+ * <dt>{@link #SIM_STATE_NETWORK_LOCKED}</dt>
+ * <dd>SIM card network locked</dd>
+ * <dt>{@link #SIM_STATE_PERM_DISABLED}</dt>
+ * <dd>SIM card permanently disabled due to PUK failures</dd>
+ * <dt>{@link #SIM_STATE_LOADED}</dt>
+ * <dd>SIM card data loaded</dd>
+ * </dl>
+ * </dd>
+ * </dl>
+ *
+ * <p class="note">Requires the READ_PRIVILEGED_PHONE_STATE permission.
+ *
+ * <p class="note">The current state can also be queried using
+ * {@link #getSimApplicationState()}.
+ *
+ * <p class="note">This is a protected intent that can only be sent by the system.
+ * @hide
+ */
+ @SystemApi
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_SIM_APPLICATION_STATE_CHANGED =
+ "android.telephony.action.SIM_APPLICATION_STATE_CHANGED";
+
+ /**
+ * Broadcast Action: Status of the SIM slots on the device has changed.
+ *
+ * <p class="note">Requires the READ_PRIVILEGED_PHONE_STATE permission.
+ *
+ * <p class="note">The status can be queried using
+ * {@link #getUiccSlotsInfo()}
+ *
+ * <p class="note">This is a protected intent that can only be sent by the system.
+ * @hide
+ */
+ @SystemApi
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_SIM_SLOT_STATUS_CHANGED =
+ "android.telephony.action.SIM_SLOT_STATUS_CHANGED";
/**
* @return true if a ICC card is present
@@ -2156,6 +2271,14 @@
* @see #SIM_STATE_CARD_RESTRICTED
*/
public int getSimState() {
+ int simState = getSimStateIncludingLoaded();
+ if (simState == SIM_STATE_LOADED) {
+ simState = SIM_STATE_READY;
+ }
+ return simState;
+ }
+
+ private int getSimStateIncludingLoaded() {
int slotIndex = getSlotIndex();
// slotIndex may be invalid due to sim being absent. In that case query all slots to get
// sim state
@@ -2174,7 +2297,63 @@
"state as absent");
return SIM_STATE_ABSENT;
}
- return getSimState(slotIndex);
+ return SubscriptionManager.getSimStateForSlotIndex(slotIndex);
+ }
+
+ /**
+ * Returns a constant indicating the state of the default SIM card.
+ *
+ * @see #SIM_STATE_UNKNOWN
+ * @see #SIM_STATE_ABSENT
+ * @see #SIM_STATE_CARD_IO_ERROR
+ * @see #SIM_STATE_CARD_RESTRICTED
+ * @see #SIM_STATE_PRESENT
+ *
+ * @hide
+ */
+ @SystemApi
+ public int getSimCardState() {
+ int simCardState = getSimState();
+ switch (simCardState) {
+ case SIM_STATE_UNKNOWN:
+ case SIM_STATE_ABSENT:
+ case SIM_STATE_CARD_IO_ERROR:
+ case SIM_STATE_CARD_RESTRICTED:
+ return simCardState;
+ default:
+ return SIM_STATE_PRESENT;
+ }
+ }
+
+ /**
+ * Returns a constant indicating the state of the card applications on the default SIM card.
+ *
+ * @see #SIM_STATE_UNKNOWN
+ * @see #SIM_STATE_PIN_REQUIRED
+ * @see #SIM_STATE_PUK_REQUIRED
+ * @see #SIM_STATE_NETWORK_LOCKED
+ * @see #SIM_STATE_NOT_READY
+ * @see #SIM_STATE_PERM_DISABLED
+ * @see #SIM_STATE_LOADED
+ *
+ * @hide
+ */
+ @SystemApi
+ public int getSimApplicationState() {
+ int simApplicationState = getSimStateIncludingLoaded();
+ switch (simApplicationState) {
+ case SIM_STATE_UNKNOWN:
+ case SIM_STATE_ABSENT:
+ case SIM_STATE_CARD_IO_ERROR:
+ case SIM_STATE_CARD_RESTRICTED:
+ return SIM_STATE_UNKNOWN;
+ case SIM_STATE_READY:
+ // Ready is not a valid state anymore. The state that is broadcast goes from
+ // NOT_READY to either LOCKED or LOADED.
+ return SIM_STATE_NOT_READY;
+ default:
+ return simApplicationState;
+ }
}
/**
@@ -2195,6 +2374,9 @@
*/
public int getSimState(int slotIndex) {
int simState = SubscriptionManager.getSimStateForSlotIndex(slotIndex);
+ if (simState == SIM_STATE_LOADED) {
+ simState = SIM_STATE_READY;
+ }
return simState;
}
@@ -2416,6 +2598,53 @@
}
}
+ /**
+ * Gets all the UICC slots.
+ *
+ * @return UiccSlotInfo array.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ public UiccSlotInfo[] getUiccSlotsInfo() {
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony == null) {
+ return null;
+ }
+ return telephony.getUiccSlotsInfo();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Map logicalSlot to physicalSlot, and activate the physicalSlot if it is inactive. For
+ * example, passing the physicalSlots array [1, 0] means mapping the first item 1, which is
+ * physical slot index 1, to the logical slot 0; and mapping the second item 0, which is
+ * physical slot index 0, to the logical slot 1. The index of the array means the index of the
+ * logical slots.
+ *
+ * @param physicalSlots Index i in the array representing physical slot for phone i. The array
+ * size should be same as {@link #getPhoneCount()}.
+ * @return boolean Return true if the switch succeeds, false if the switch fails.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ public boolean switchSlots(int[] physicalSlots) {
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony == null) {
+ return false;
+ }
+ return telephony.switchSlots(physicalSlots);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
//
//
// Subscriber Info
@@ -2503,6 +2732,33 @@
}
}
+ /**
+ * Resets the Carrier Keys in the database. This involves 2 steps:
+ * 1. Delete the keys from the database.
+ * 2. Send an intent to download new Certificates.
+ * <p>
+ * Requires Permission:
+ * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
+ * @hide
+ */
+ public void resetCarrierKeysForImsiEncryption() {
+ try {
+ IPhoneSubInfo info = getSubscriberInfo();
+ if (info == null) {
+ throw new RuntimeException("IMSI error: Subscriber Info is null");
+ }
+ int subId = getSubId(SubscriptionManager.getDefaultDataSubscriptionId());
+ info.resetCarrierKeysForImsiEncryption(subId, mContext.getOpPackageName());
+ } catch (RemoteException ex) {
+ Rlog.e(TAG, "getCarrierInfoForImsiEncryption RemoteException" + ex);
+ throw new RuntimeException("IMSI error: Remote Exception");
+ } catch (NullPointerException ex) {
+ // This could happen before phone restarts due to crashing
+ Rlog.e(TAG, "getCarrierInfoForImsiEncryption NullPointerException" + ex);
+ throw new RuntimeException("IMSI error: Null Pointer exception");
+ }
+ }
+
/**
* @param keyAvailability bitmask that defines the availabilty of keys for a type.
* @param keyType the key type which is being checked. (WLAN, EPDG)
@@ -2538,7 +2794,7 @@
* device keystore.
* <p>
* Requires Permission:
- * {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
+ * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
* @param imsiEncryptionInfo which includes the Key Type, the Public Key
* (java.security.PublicKey) and the Key Identifier.and the Key Identifier.
* The keyIdentifier Attribute value pair that helps a server locate
@@ -4921,6 +5177,25 @@
}
/**
+ * @return the {@IImsRegistration} interface that corresponds with the slot index and feature.
+ * @param slotIndex The SIM slot corresponding to the ImsService ImsRegistration is active for.
+ * @param feature An integer indicating the feature that we wish to get the ImsRegistration for.
+ * Corresponds to features defined in ImsFeature.
+ * @hide
+ */
+ public @Nullable IImsRegistration getImsRegistration(int slotIndex, int feature) {
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ return telephony.getImsRegistration(slotIndex, feature);
+ }
+ } catch (RemoteException e) {
+ Rlog.e(TAG, "getImsRegistration, RemoteException: " + e.getMessage());
+ }
+ return null;
+ }
+
+ /**
* Set IMS registration state
*
* @param Registration state
@@ -6253,8 +6528,10 @@
* <p>Requires Permission:
* {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
*
- * @hide
+ * {@hide}
**/
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
public void setSimPowerState(int state) {
setSimPowerStateForSlot(getSlotIndex(), state);
}
@@ -6273,8 +6550,10 @@
* <p>Requires Permission:
* {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
*
- * @hide
+ * {@hide}
**/
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
public void setSimPowerStateForSlot(int slotIndex, int state) {
try {
ITelephony telephony = getITelephony();
@@ -6750,14 +7029,19 @@
/**
* Returns carrier id of the current subscription.
- * <p>To recognize a carrier (including MVNO) as a first class identity, assign each carrier
- * with a canonical integer a.k.a carrier id.
+ * <p>To recognize a carrier (including MVNO) as a first-class identity, Android assigns each
+ * carrier with a canonical integer a.k.a. android carrier id. The Android carrier ID is an
+ * Android platform-wide identifier for a carrier. AOSP maintains carrier ID assignments in
+ * <a href="https://android.googlesource.com/platform/packages/providers/TelephonyProvider/+/master/assets/carrier_list.textpb">here</a>
+ *
+ * <p>Apps which have carrier-specific configurations or business logic can use the carrier id
+ * as an Android platform-wide identifier for carriers.
*
* @return Carrier id of the current subscription. Return {@link #UNKNOWN_CARRIER_ID} if the
* subscription is unavailable or the carrier cannot be identified.
* @throws IllegalStateException if telephony service is unavailable.
*/
- public int getSubscriptionCarrierId() {
+ public int getAndroidCarrierIdForSubscription() {
try {
ITelephony service = getITelephony();
return service.getSubscriptionCarrierId(getSubId());
@@ -6773,17 +7057,18 @@
/**
* Returns carrier name of the current subscription.
- * <p>Carrier name is a user-facing name of carrier id {@link #getSubscriptionCarrierId()},
- * usually the brand name of the subsidiary (e.g. T-Mobile). Each carrier could configure
- * multiple {@link #getSimOperatorName() SPN} but should have a single carrier name.
- * Carrier name is not a canonical identity, use {@link #getSubscriptionCarrierId()} instead.
+ * <p>Carrier name is a user-facing name of carrier id
+ * {@link #getAndroidCarrierIdForSubscription()}, usually the brand name of the subsidiary
+ * (e.g. T-Mobile). Each carrier could configure multiple {@link #getSimOperatorName() SPN} but
+ * should have a single carrier name. Carrier name is not a canonical identity,
+ * use {@link #getAndroidCarrierIdForSubscription()} instead.
* <p>The returned carrier name is unlocalized.
*
* @return Carrier name of the current subscription. Return {@code null} if the subscription is
* unavailable or the carrier cannot be identified.
* @throws IllegalStateException if telephony service is unavailable.
*/
- public String getSubscriptionCarrierName() {
+ public CharSequence getAndroidCarrierNameForSubscription() {
try {
ITelephony service = getITelephony();
return service.getSubscriptionCarrierName(getSubId());
diff --git a/android/telephony/UiccAccessRule.java b/android/telephony/UiccAccessRule.java
index e42a758..3937201 100644
--- a/android/telephony/UiccAccessRule.java
+++ b/android/telephony/UiccAccessRule.java
@@ -32,6 +32,7 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
+import java.util.Objects;
/**
* Describes a single UICC access rule according to the GlobalPlatform Secure Element Access Control
@@ -205,6 +206,21 @@
}
@Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ UiccAccessRule that = (UiccAccessRule) obj;
+ return Arrays.equals(mCertificateHash, that.mCertificateHash)
+ && Objects.equals(mPackageName, that.mPackageName)
+ && mAccessType == that.mAccessType;
+ }
+
+ @Override
public String toString() {
return "cert: " + IccUtils.bytesToHexString(mCertificateHash) + " pkg: " +
mPackageName + " access: " + mAccessType;
diff --git a/android/telephony/UiccSlotInfo.java b/android/telephony/UiccSlotInfo.java
new file mode 100644
index 0000000..0b3cbad
--- /dev/null
+++ b/android/telephony/UiccSlotInfo.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2018 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.telephony;
+
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+import android.annotation.IntDef;
+
+/**
+ * Class for the information of a UICC slot.
+ * @hide
+ */
+@SystemApi
+public class UiccSlotInfo implements Parcelable {
+ /**
+ * Card state.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "CARD_STATE_INFO_" }, value = {
+ CARD_STATE_INFO_ABSENT,
+ CARD_STATE_INFO_PRESENT,
+ CARD_STATE_INFO_ERROR,
+ CARD_STATE_INFO_RESTRICTED
+ })
+ public @interface CardStateInfo {}
+
+ /** Card state absent. */
+ public static final int CARD_STATE_INFO_ABSENT = 1;
+
+ /** Card state present. */
+ public static final int CARD_STATE_INFO_PRESENT = 2;
+
+ /** Card state error. */
+ public static final int CARD_STATE_INFO_ERROR = 3;
+
+ /** Card state restricted. */
+ public static final int CARD_STATE_INFO_RESTRICTED = 4;
+
+ public final boolean isActive;
+ public final boolean isEuicc;
+ public final String cardId;
+ public final @CardStateInfo int cardStateInfo;
+
+ public static final Creator<UiccSlotInfo> CREATOR = new Creator<UiccSlotInfo>() {
+ @Override
+ public UiccSlotInfo createFromParcel(Parcel in) {
+ return new UiccSlotInfo(in);
+ }
+
+ @Override
+ public UiccSlotInfo[] newArray(int size) {
+ return new UiccSlotInfo[size];
+ }
+ };
+
+ private UiccSlotInfo(Parcel in) {
+ isActive = in.readByte() != 0;
+ isEuicc = in.readByte() != 0;
+ cardId = in.readString();
+ cardStateInfo = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByte((byte) (isActive ? 1 : 0));
+ dest.writeByte((byte) (isEuicc ? 1 : 0));
+ dest.writeString(cardId);
+ dest.writeInt(cardStateInfo);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public UiccSlotInfo(boolean isActive, boolean isEuicc, String cardId,
+ @CardStateInfo int cardStateInfo) {
+ this.isActive = isActive;
+ this.isEuicc = isEuicc;
+ this.cardId = cardId;
+ this.cardStateInfo = cardStateInfo;
+ }
+
+ public boolean getIsActive() {
+ return isActive;
+ }
+
+ public boolean getIsEuicc() {
+ return isEuicc;
+ }
+
+ public String getCardId() {
+ return cardId;
+ }
+
+ @CardStateInfo
+ public int getCardStateInfo() {
+ return cardStateInfo;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ UiccSlotInfo that = (UiccSlotInfo) obj;
+ return (isActive == that.isActive)
+ && (isEuicc == that.isEuicc)
+ && (cardId == that.cardId)
+ && (cardStateInfo == that.cardStateInfo);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + (isActive ? 1 : 0);
+ result = 31 * result + (isEuicc ? 1 : 0);
+ result = 31 * result + Objects.hashCode(cardId);
+ result = 31 * result + cardStateInfo;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "UiccSlotInfo (isActive="
+ + isActive
+ + ", isEuicc="
+ + isEuicc
+ + ", cardId="
+ + cardId
+ + ", cardState="
+ + cardStateInfo
+ + ")";
+ }
+}
diff --git a/android/telephony/VisualVoicemailSmsFilterSettings.java b/android/telephony/VisualVoicemailSmsFilterSettings.java
index 8ed96a3..7eeb1ce 100644
--- a/android/telephony/VisualVoicemailSmsFilterSettings.java
+++ b/android/telephony/VisualVoicemailSmsFilterSettings.java
@@ -15,12 +15,10 @@
*/
package android.telephony;
-import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
-
-import android.telecom.PhoneAccountHandle;
import android.telephony.VisualVoicemailService.VisualVoicemailTask;
+
import java.util.Collections;
import java.util.List;
@@ -75,6 +73,7 @@
private String mClientPrefix = DEFAULT_CLIENT_PREFIX;
private List<String> mOriginatingNumbers = DEFAULT_ORIGINATING_NUMBERS;
private int mDestinationPort = DEFAULT_DESTINATION_PORT;
+ private String mPackageName;
public VisualVoicemailSmsFilterSettings build() {
return new VisualVoicemailSmsFilterSettings(this);
@@ -116,6 +115,15 @@
return this;
}
+ /**
+ * The package that registered this filter.
+ *
+ * @hide
+ */
+ public Builder setPackageName(String packageName) {
+ mPackageName = packageName;
+ return this;
+ }
}
/**
@@ -138,12 +146,20 @@
public final int destinationPort;
/**
+ * The package that registered this filter.
+ *
+ * @hide
+ */
+ public final String packageName;
+
+ /**
* Use {@link Builder} to construct
*/
private VisualVoicemailSmsFilterSettings(Builder builder) {
clientPrefix = builder.mClientPrefix;
originatingNumbers = builder.mOriginatingNumbers;
destinationPort = builder.mDestinationPort;
+ packageName = builder.mPackageName;
}
public static final Creator<VisualVoicemailSmsFilterSettings> CREATOR =
@@ -154,7 +170,7 @@
builder.setClientPrefix(in.readString());
builder.setOriginatingNumbers(in.createStringArrayList());
builder.setDestinationPort(in.readInt());
-
+ builder.setPackageName(in.readString());
return builder.build();
}
@@ -174,10 +190,11 @@
dest.writeString(clientPrefix);
dest.writeStringList(originatingNumbers);
dest.writeInt(destinationPort);
+ dest.writeString(packageName);
}
@Override
- public String toString(){
+ public String toString() {
return "[VisualVoicemailSmsFilterSettings "
+ "clientPrefix=" + clientPrefix
+ ", originatingNumbers=" + originatingNumbers
diff --git a/android/telephony/data/ApnSetting.java b/android/telephony/data/ApnSetting.java
new file mode 100644
index 0000000..73a05af
--- /dev/null
+++ b/android/telephony/data/ApnSetting.java
@@ -0,0 +1,1351 @@
+/*
+ * Copyright (C) 2018 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.telephony.data;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.StringDef;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.hardware.radio.V1_0.ApnTypes;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.Telephony;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A class representing an APN configuration.
+ */
+public class ApnSetting implements Parcelable {
+
+ static final String LOG_TAG = "ApnSetting";
+ private static final boolean VDBG = false;
+
+ private final String mEntryName;
+ private final String mApnName;
+ private final InetAddress mProxy;
+ private final int mPort;
+ private final URL mMmsc;
+ private final InetAddress mMmsProxy;
+ private final int mMmsPort;
+ private final String mUser;
+ private final String mPassword;
+ private final int mAuthType;
+ private final List<String> mTypes;
+ private final int mTypesBitmap;
+ private final int mId;
+ private final String mOperatorNumeric;
+ private final String mProtocol;
+ private final String mRoamingProtocol;
+ private final int mMtu;
+
+ private final boolean mCarrierEnabled;
+
+ private final int mNetworkTypeBitmask;
+
+ private final int mProfileId;
+
+ private final boolean mModemCognitive;
+ private final int mMaxConns;
+ private final int mWaitTime;
+ private final int mMaxConnsTime;
+
+ private final String mMvnoType;
+ private final String mMvnoMatchData;
+
+ private boolean mPermanentFailed = false;
+
+ /**
+ * Returns the types bitmap of the APN.
+ *
+ * @return types bitmap of the APN
+ * @hide
+ */
+ public int getTypesBitmap() {
+ return mTypesBitmap;
+ }
+
+ /**
+ * Returns the MTU size of the mobile interface to which the APN connected.
+ *
+ * @return the MTU size of the APN
+ * @hide
+ */
+ public int getMtu() {
+ return mMtu;
+ }
+
+ /**
+ * Returns the profile id to which the APN saved in modem.
+ *
+ * @return the profile id of the APN
+ * @hide
+ */
+ public int getProfileId() {
+ return mProfileId;
+ }
+
+ /**
+ * Returns if the APN setting is to be set in modem.
+ *
+ * @return is the APN setting to be set in modem
+ * @hide
+ */
+ public boolean getModemCognitive() {
+ return mModemCognitive;
+ }
+
+ /**
+ * Returns the max connections of this APN.
+ *
+ * @return the max connections of this APN
+ * @hide
+ */
+ public int getMaxConns() {
+ return mMaxConns;
+ }
+
+ /**
+ * Returns the wait time for retry of the APN.
+ *
+ * @return the wait time for retry of the APN
+ * @hide
+ */
+ public int getWaitTime() {
+ return mWaitTime;
+ }
+
+ /**
+ * Returns the time to limit max connection for the APN.
+ *
+ * @return the time to limit max connection for the APN
+ * @hide
+ */
+ public int getMaxConnsTime() {
+ return mMaxConnsTime;
+ }
+
+ /**
+ * Returns the MVNO data. Examples:
+ * "spn": A MOBILE, BEN NL
+ * "imsi": 302720x94, 2060188
+ * "gid": 4E, 33
+ * "iccid": 898603 etc..
+ *
+ * @return the mvno match data
+ * @hide
+ */
+ public String getMvnoMatchData() {
+ return mMvnoMatchData;
+ }
+
+ /**
+ * Indicates this APN setting is permanently failed and cannot be
+ * retried by the retry manager anymore.
+ *
+ * @return if this APN setting is permanently failed
+ * @hide
+ */
+ public boolean getPermanentFailed() {
+ return mPermanentFailed;
+ }
+
+ /**
+ * Sets if this APN setting is permanently failed.
+ *
+ * @param permanentFailed if this APN setting is permanently failed
+ * @hide
+ */
+ public void setPermanentFailed(boolean permanentFailed) {
+ mPermanentFailed = permanentFailed;
+ }
+
+ /**
+ * Returns the entry name of the APN.
+ *
+ * @return the entry name for the APN
+ */
+ public String getEntryName() {
+ return mEntryName;
+ }
+
+ /**
+ * Returns the name of the APN.
+ *
+ * @return APN name
+ */
+ public String getApnName() {
+ return mApnName;
+ }
+
+ /**
+ * Returns the proxy address of the APN.
+ *
+ * @return proxy address.
+ */
+ public InetAddress getProxy() {
+ return mProxy;
+ }
+
+ /**
+ * Returns the proxy port of the APN.
+ *
+ * @return proxy port
+ */
+ public int getPort() {
+ return mPort;
+ }
+ /**
+ * Returns the MMSC URL of the APN.
+ *
+ * @return MMSC URL.
+ */
+ public URL getMmsc() {
+ return mMmsc;
+ }
+
+ /**
+ * Returns the MMS proxy address of the APN.
+ *
+ * @return MMS proxy address.
+ */
+ public InetAddress getMmsProxy() {
+ return mMmsProxy;
+ }
+
+ /**
+ * Returns the MMS proxy port of the APN.
+ *
+ * @return MMS proxy port
+ */
+ public int getMmsPort() {
+ return mMmsPort;
+ }
+
+ /**
+ * Returns the APN username of the APN.
+ *
+ * @return APN username
+ */
+ public String getUser() {
+ return mUser;
+ }
+
+ /**
+ * Returns the APN password of the APN.
+ *
+ * @return APN password
+ */
+ public String getPassword() {
+ return mPassword;
+ }
+
+ /** @hide */
+ @IntDef({
+ AUTH_TYPE_NONE,
+ AUTH_TYPE_PAP,
+ AUTH_TYPE_CHAP,
+ AUTH_TYPE_PAP_OR_CHAP,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AuthType {}
+
+ /**
+ * Returns the authentication type of the APN.
+ *
+ * Example of possible values: {@link #AUTH_TYPE_NONE}, {@link #AUTH_TYPE_PAP}.
+ *
+ * @return authentication type
+ */
+ @AuthType
+ public int getAuthType() {
+ return mAuthType;
+ }
+
+ /** @hide */
+ @StringDef({
+ TYPE_DEFAULT,
+ TYPE_MMS,
+ TYPE_SUPL,
+ TYPE_DUN,
+ TYPE_HIPRI,
+ TYPE_FOTA,
+ TYPE_IMS,
+ TYPE_CBS,
+ TYPE_IA,
+ TYPE_EMERGENCY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ApnType {}
+
+ /**
+ * Returns the list of APN types of the APN.
+ *
+ * Example of possible values: {@link #TYPE_DEFAULT}, {@link #TYPE_MMS}.
+ *
+ * @return the list of APN types
+ */
+ @ApnType
+ public List<String> getTypes() {
+ return mTypes;
+ }
+
+ /**
+ * Returns the unique database id for this entry.
+ *
+ * @return the unique database id
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the numeric operator ID for the APN. Usually
+ * {@link android.provider.Telephony.Carriers#MCC} +
+ * {@link android.provider.Telephony.Carriers#MNC}.
+ *
+ * @return the numeric operator ID
+ */
+ public String getOperatorNumeric() {
+ return mOperatorNumeric;
+ }
+
+ /** @hide */
+ @StringDef({
+ PROTOCOL_IP,
+ PROTOCOL_IPV6,
+ PROTOCOL_IPV4V6,
+ PROTOCOL_PPP,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ProtocolType {}
+
+ /**
+ * Returns the protocol to use to connect to this APN.
+ *
+ * One of the {@code PDP_type} values in TS 27.007 section 10.1.1.
+ * Example of possible values: {@link #PROTOCOL_IP}, {@link #PROTOCOL_IPV6}.
+ *
+ * @return the protocol
+ */
+ @ProtocolType
+ public String getProtocol() {
+ return mProtocol;
+ }
+
+ /**
+ * Returns the protocol to use to connect to this APN when roaming.
+ *
+ * The syntax is the same as {@link android.provider.Telephony.Carriers#PROTOCOL}.
+ *
+ * @return the roaming protocol
+ */
+ public String getRoamingProtocol() {
+ return mRoamingProtocol;
+ }
+
+ /**
+ * Returns the current status of APN.
+ *
+ * {@code true} : enabled APN.
+ * {@code false} : disabled APN.
+ *
+ * @return the current status
+ */
+ public boolean isEnabled() {
+ return mCarrierEnabled;
+ }
+
+ /**
+ * Returns a bitmask describing the Radio Technologies(Network Types) which this APN may use.
+ *
+ * NetworkType bitmask is calculated from NETWORK_TYPE defined in {@link TelephonyManager}.
+ *
+ * Examples of Network Types include {@link TelephonyManager#NETWORK_TYPE_UNKNOWN},
+ * {@link TelephonyManager#NETWORK_TYPE_GPRS}, {@link TelephonyManager#NETWORK_TYPE_EDGE}.
+ *
+ * @return a bitmask describing the Radio Technologies(Network Types)
+ */
+ public int getNetworkTypeBitmask() {
+ return mNetworkTypeBitmask;
+ }
+
+ /** @hide */
+ @StringDef({
+ MVNO_TYPE_SPN,
+ MVNO_TYPE_IMSI,
+ MVNO_TYPE_GID,
+ MVNO_TYPE_ICCID,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface MvnoType {}
+
+ /**
+ * Returns the MVNO match type for this APN.
+ *
+ * Example of possible values: {@link #MVNO_TYPE_SPN}, {@link #MVNO_TYPE_IMSI}.
+ *
+ * @return the MVNO match type
+ */
+ @MvnoType
+ public String getMvnoType() {
+ return mMvnoType;
+ }
+
+ private ApnSetting(Builder builder) {
+ this.mEntryName = builder.mEntryName;
+ this.mApnName = builder.mApnName;
+ this.mProxy = builder.mProxy;
+ this.mPort = builder.mPort;
+ this.mMmsc = builder.mMmsc;
+ this.mMmsProxy = builder.mMmsProxy;
+ this.mMmsPort = builder.mMmsPort;
+ this.mUser = builder.mUser;
+ this.mPassword = builder.mPassword;
+ this.mAuthType = builder.mAuthType;
+ this.mTypes = (builder.mTypes == null ? new ArrayList<String>() : builder.mTypes);
+ this.mTypesBitmap = builder.mTypesBitmap;
+ this.mId = builder.mId;
+ this.mOperatorNumeric = builder.mOperatorNumeric;
+ this.mProtocol = builder.mProtocol;
+ this.mRoamingProtocol = builder.mRoamingProtocol;
+ this.mMtu = builder.mMtu;
+ this.mCarrierEnabled = builder.mCarrierEnabled;
+ this.mNetworkTypeBitmask = builder.mNetworkTypeBitmask;
+ this.mProfileId = builder.mProfileId;
+ this.mModemCognitive = builder.mModemCognitive;
+ this.mMaxConns = builder.mMaxConns;
+ this.mWaitTime = builder.mWaitTime;
+ this.mMaxConnsTime = builder.mMaxConnsTime;
+ this.mMvnoType = builder.mMvnoType;
+ this.mMvnoMatchData = builder.mMvnoMatchData;
+ }
+
+ /** @hide */
+ public static ApnSetting makeApnSetting(int id, String operatorNumeric, String entryName,
+ String apnName, InetAddress proxy, int port, URL mmsc, InetAddress mmsProxy,
+ int mmsPort, String user, String password, int authType, List<String> types,
+ String protocol, String roamingProtocol, boolean carrierEnabled,
+ int networkTypeBitmask, int profileId, boolean modemCognitive, int maxConns,
+ int waitTime, int maxConnsTime, int mtu, String mvnoType, String mvnoMatchData) {
+ return new Builder()
+ .setId(id)
+ .setOperatorNumeric(operatorNumeric)
+ .setEntryName(entryName)
+ .setApnName(apnName)
+ .setProxy(proxy)
+ .setPort(port)
+ .setMmsc(mmsc)
+ .setMmsProxy(mmsProxy)
+ .setMmsPort(mmsPort)
+ .setUser(user)
+ .setPassword(password)
+ .setAuthType(authType)
+ .setTypes(types)
+ .setProtocol(protocol)
+ .setRoamingProtocol(roamingProtocol)
+ .setCarrierEnabled(carrierEnabled)
+ .setNetworkTypeBitmask(networkTypeBitmask)
+ .setProfileId(profileId)
+ .setModemCognitive(modemCognitive)
+ .setMaxConns(maxConns)
+ .setWaitTime(waitTime)
+ .setMaxConnsTime(maxConnsTime)
+ .setMtu(mtu)
+ .setMvnoType(mvnoType)
+ .setMvnoMatchData(mvnoMatchData)
+ .build();
+ }
+
+ /** @hide */
+ public static ApnSetting makeApnSetting(Cursor cursor) {
+ String[] types = parseTypes(
+ cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.TYPE)));
+ int networkTypeBitmask = cursor.getInt(
+ cursor.getColumnIndexOrThrow(Telephony.Carriers.NETWORK_TYPE_BITMASK));
+ if (networkTypeBitmask == 0) {
+ final int bearerBitmask = cursor.getInt(cursor.getColumnIndexOrThrow(
+ Telephony.Carriers.BEARER_BITMASK));
+ networkTypeBitmask =
+ ServiceState.convertBearerBitmaskToNetworkTypeBitmask(bearerBitmask);
+ }
+
+ return makeApnSetting(
+ cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers._ID)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.NUMERIC)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.NAME)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.APN)),
+ inetAddressFromString(cursor.getString(
+ cursor.getColumnIndexOrThrow(Telephony.Carriers.PROXY))),
+ portFromString(cursor.getString(
+ cursor.getColumnIndexOrThrow(Telephony.Carriers.PORT))),
+ URLFromString(cursor.getString(
+ cursor.getColumnIndexOrThrow(Telephony.Carriers.MMSC))),
+ inetAddressFromString(cursor.getString(
+ cursor.getColumnIndexOrThrow(Telephony.Carriers.MMSPROXY))),
+ portFromString(cursor.getString(
+ cursor.getColumnIndexOrThrow(Telephony.Carriers.MMSPORT))),
+ cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.USER)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.PASSWORD)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.AUTH_TYPE)),
+ Arrays.asList(types),
+ cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.PROTOCOL)),
+ cursor.getString(cursor.getColumnIndexOrThrow(
+ Telephony.Carriers.ROAMING_PROTOCOL)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(
+ Telephony.Carriers.CARRIER_ENABLED)) == 1,
+ networkTypeBitmask,
+ cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.PROFILE_ID)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(
+ Telephony.Carriers.MODEM_COGNITIVE)) == 1,
+ cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.MAX_CONNS)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.WAIT_TIME)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(
+ Telephony.Carriers.MAX_CONNS_TIME)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.MTU)),
+ cursor.getString(cursor.getColumnIndexOrThrow(
+ Telephony.Carriers.MVNO_TYPE)),
+ cursor.getString(cursor.getColumnIndexOrThrow(
+ Telephony.Carriers.MVNO_MATCH_DATA)));
+ }
+
+ /** @hide */
+ public static ApnSetting makeApnSetting(ApnSetting apn) {
+ return makeApnSetting(apn.mId, apn.mOperatorNumeric, apn.mEntryName, apn.mApnName,
+ apn.mProxy, apn.mPort, apn.mMmsc, apn.mMmsProxy, apn.mMmsPort, apn.mUser,
+ apn.mPassword, apn.mAuthType, apn.mTypes, apn.mProtocol, apn.mRoamingProtocol,
+ apn.mCarrierEnabled, apn.mNetworkTypeBitmask, apn.mProfileId,
+ apn.mModemCognitive, apn.mMaxConns, apn.mWaitTime, apn.mMaxConnsTime, apn.mMtu,
+ apn.mMvnoType, apn.mMvnoMatchData);
+ }
+
+ /** @hide */
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("[ApnSettingV4] ")
+ .append(mEntryName)
+ .append(", ").append(mId)
+ .append(", ").append(mOperatorNumeric)
+ .append(", ").append(mApnName)
+ .append(", ").append(inetAddressToString(mProxy))
+ .append(", ").append(URLToString(mMmsc))
+ .append(", ").append(inetAddressToString(mMmsProxy))
+ .append(", ").append(portToString(mMmsPort))
+ .append(", ").append(portToString(mPort))
+ .append(", ").append(mAuthType).append(", ");
+ for (int i = 0; i < mTypes.size(); i++) {
+ sb.append(mTypes.get(i));
+ if (i < mTypes.size() - 1) {
+ sb.append(" | ");
+ }
+ }
+ sb.append(", ").append(mProtocol);
+ sb.append(", ").append(mRoamingProtocol);
+ sb.append(", ").append(mCarrierEnabled);
+ sb.append(", ").append(mProfileId);
+ sb.append(", ").append(mModemCognitive);
+ sb.append(", ").append(mMaxConns);
+ sb.append(", ").append(mWaitTime);
+ sb.append(", ").append(mMaxConnsTime);
+ sb.append(", ").append(mMtu);
+ sb.append(", ").append(mMvnoType);
+ sb.append(", ").append(mMvnoMatchData);
+ sb.append(", ").append(mPermanentFailed);
+ sb.append(", ").append(mNetworkTypeBitmask);
+ return sb.toString();
+ }
+
+ /**
+ * Returns true if there are MVNO params specified.
+ * @hide
+ */
+ public boolean hasMvnoParams() {
+ return !TextUtils.isEmpty(mMvnoType) && !TextUtils.isEmpty(mMvnoMatchData);
+ }
+
+ /** @hide */
+ public boolean canHandleType(String type) {
+ if (!mCarrierEnabled) return false;
+ boolean wildcardable = true;
+ if (TYPE_IA.equalsIgnoreCase(type)) wildcardable = false;
+ for (String t : mTypes) {
+ // DEFAULT handles all, and HIPRI is handled by DEFAULT
+ if (t.equalsIgnoreCase(type)
+ || (wildcardable && t.equalsIgnoreCase(TYPE_ALL))
+ || (t.equalsIgnoreCase(TYPE_DEFAULT)
+ && type.equalsIgnoreCase(TYPE_HIPRI))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // check whether the types of two APN same (even only one type of each APN is same)
+ private boolean typeSameAny(ApnSetting first, ApnSetting second) {
+ if (VDBG) {
+ StringBuilder apnType1 = new StringBuilder(first.mApnName + ": ");
+ for (int index1 = 0; index1 < first.mTypes.size(); index1++) {
+ apnType1.append(first.mTypes.get(index1));
+ apnType1.append(",");
+ }
+
+ StringBuilder apnType2 = new StringBuilder(second.mApnName + ": ");
+ for (int index1 = 0; index1 < second.mTypes.size(); index1++) {
+ apnType2.append(second.mTypes.get(index1));
+ apnType2.append(",");
+ }
+ Rlog.d(LOG_TAG, "APN1: is " + apnType1);
+ Rlog.d(LOG_TAG, "APN2: is " + apnType2);
+ }
+
+ for (int index1 = 0; index1 < first.mTypes.size(); index1++) {
+ for (int index2 = 0; index2 < second.mTypes.size(); index2++) {
+ if (first.mTypes.get(index1).equals(ApnSetting.TYPE_ALL)
+ || second.mTypes.get(index2).equals(ApnSetting.TYPE_ALL)
+ || first.mTypes.get(index1).equals(second.mTypes.get(index2))) {
+ if (VDBG) Rlog.d(LOG_TAG, "typeSameAny: return true");
+ return true;
+ }
+ }
+ }
+
+ if (VDBG) Rlog.d(LOG_TAG, "typeSameAny: return false");
+ return false;
+ }
+
+ // TODO - if we have this function we should also have hashCode.
+ // Also should handle changes in type order and perhaps case-insensitivity
+ /** @hide */
+ public boolean equals(Object o) {
+ if (o instanceof ApnSetting == false) {
+ return false;
+ }
+
+ ApnSetting other = (ApnSetting) o;
+
+ return mEntryName.equals(other.mEntryName)
+ && Objects.equals(mId, other.mId)
+ && Objects.equals(mOperatorNumeric, other.mOperatorNumeric)
+ && Objects.equals(mApnName, other.mApnName)
+ && Objects.equals(mProxy, other.mProxy)
+ && Objects.equals(mMmsc, other.mMmsc)
+ && Objects.equals(mMmsProxy, other.mMmsProxy)
+ && Objects.equals(mMmsPort, other.mMmsPort)
+ && Objects.equals(mPort,other.mPort)
+ && Objects.equals(mUser, other.mUser)
+ && Objects.equals(mPassword, other.mPassword)
+ && Objects.equals(mAuthType, other.mAuthType)
+ && Objects.equals(mTypes, other.mTypes)
+ && Objects.equals(mTypesBitmap, other.mTypesBitmap)
+ && Objects.equals(mProtocol, other.mProtocol)
+ && Objects.equals(mRoamingProtocol, other.mRoamingProtocol)
+ && Objects.equals(mCarrierEnabled, other.mCarrierEnabled)
+ && Objects.equals(mProfileId, other.mProfileId)
+ && Objects.equals(mModemCognitive, other.mModemCognitive)
+ && Objects.equals(mMaxConns, other.mMaxConns)
+ && Objects.equals(mWaitTime, other.mWaitTime)
+ && Objects.equals(mMaxConnsTime, other.mMaxConnsTime)
+ && Objects.equals(mMtu, other.mMtu)
+ && Objects.equals(mMvnoType, other.mMvnoType)
+ && Objects.equals(mMvnoMatchData, other.mMvnoMatchData)
+ && Objects.equals(mNetworkTypeBitmask, other.mNetworkTypeBitmask);
+ }
+
+ /**
+ * Compare two APN settings
+ *
+ * Note: This method does not compare 'mId', 'mNetworkTypeBitmask'. We only use this for
+ * determining if tearing a data call is needed when conditions change. See
+ * cleanUpConnectionsOnUpdatedApns in DcTracker.
+ *
+ * @param o the other object to compare
+ * @param isDataRoaming True if the device is on data roaming
+ * @return True if the two APN settings are same
+ * @hide
+ */
+ public boolean equals(Object o, boolean isDataRoaming) {
+ if (!(o instanceof ApnSetting)) {
+ return false;
+ }
+
+ ApnSetting other = (ApnSetting) o;
+
+ return mEntryName.equals(other.mEntryName)
+ && Objects.equals(mOperatorNumeric, other.mOperatorNumeric)
+ && Objects.equals(mApnName, other.mApnName)
+ && Objects.equals(mProxy, other.mProxy)
+ && Objects.equals(mMmsc, other.mMmsc)
+ && Objects.equals(mMmsProxy, other.mMmsProxy)
+ && Objects.equals(mMmsPort, other.mMmsPort)
+ && Objects.equals(mPort, other.mPort)
+ && Objects.equals(mUser, other.mUser)
+ && Objects.equals(mPassword, other.mPassword)
+ && Objects.equals(mAuthType, other.mAuthType)
+ && Objects.equals(mTypes, other.mTypes)
+ && Objects.equals(mTypesBitmap, other.mTypesBitmap)
+ && (isDataRoaming || Objects.equals(mProtocol,other.mProtocol))
+ && (!isDataRoaming || Objects.equals(mRoamingProtocol, other.mRoamingProtocol))
+ && Objects.equals(mCarrierEnabled, other.mCarrierEnabled)
+ && Objects.equals(mProfileId, other.mProfileId)
+ && Objects.equals(mModemCognitive, other.mModemCognitive)
+ && Objects.equals(mMaxConns, other.mMaxConns)
+ && Objects.equals(mWaitTime, other.mWaitTime)
+ && Objects.equals(mMaxConnsTime, other.mMaxConnsTime)
+ && Objects.equals(mMtu, other.mMtu)
+ && Objects.equals(mMvnoType, other.mMvnoType)
+ && Objects.equals(mMvnoMatchData, other.mMvnoMatchData);
+ }
+
+ /**
+ * Check if neither mention DUN and are substantially similar
+ *
+ * @param other The other APN settings to compare
+ * @return True if two APN settings are similar
+ * @hide
+ */
+ public boolean similar(ApnSetting other) {
+ return (!this.canHandleType(TYPE_DUN)
+ && !other.canHandleType(TYPE_DUN)
+ && Objects.equals(this.mApnName, other.mApnName)
+ && !typeSameAny(this, other)
+ && xorEqualsInetAddress(this.mProxy, other.mProxy)
+ && xorEqualsPort(this.mPort, other.mPort)
+ && xorEquals(this.mProtocol, other.mProtocol)
+ && xorEquals(this.mRoamingProtocol, other.mRoamingProtocol)
+ && Objects.equals(this.mCarrierEnabled, other.mCarrierEnabled)
+ && Objects.equals(this.mProfileId, other.mProfileId)
+ && Objects.equals(this.mMvnoType, other.mMvnoType)
+ && Objects.equals(this.mMvnoMatchData, other.mMvnoMatchData)
+ && xorEqualsURL(this.mMmsc, other.mMmsc)
+ && xorEqualsInetAddress(this.mMmsProxy, other.mMmsProxy)
+ && xorEqualsPort(this.mMmsPort, other.mMmsPort))
+ && Objects.equals(this.mNetworkTypeBitmask, other.mNetworkTypeBitmask);
+ }
+
+ // Equal or one is not specified.
+ private boolean xorEquals(String first, String second) {
+ return (Objects.equals(first, second)
+ || TextUtils.isEmpty(first)
+ || TextUtils.isEmpty(second));
+ }
+
+ // Equal or one is not specified.
+ private boolean xorEqualsInetAddress(InetAddress first, InetAddress second) {
+ return first == null || second == null || first.equals(second);
+ }
+
+ // Equal or one is not specified.
+ private boolean xorEqualsURL(URL first, URL second) {
+ return first == null || second == null || first.equals(second);
+ }
+
+ // Equal or one is not specified.
+ private boolean xorEqualsPort(int first, int second) {
+ return first == -1 || second == -1 || Objects.equals(first, second);
+ }
+
+ // Helper function to convert APN string into a 32-bit bitmask.
+ private static int getApnBitmask(String apn) {
+ switch (apn) {
+ case TYPE_DEFAULT: return ApnTypes.DEFAULT;
+ case TYPE_MMS: return ApnTypes.MMS;
+ case TYPE_SUPL: return ApnTypes.SUPL;
+ case TYPE_DUN: return ApnTypes.DUN;
+ case TYPE_HIPRI: return ApnTypes.HIPRI;
+ case TYPE_FOTA: return ApnTypes.FOTA;
+ case TYPE_IMS: return ApnTypes.IMS;
+ case TYPE_CBS: return ApnTypes.CBS;
+ case TYPE_IA: return ApnTypes.IA;
+ case TYPE_EMERGENCY: return ApnTypes.EMERGENCY;
+ case TYPE_ALL: return ApnTypes.ALL;
+ default: return ApnTypes.NONE;
+ }
+ }
+
+ private String deParseTypes(List<String> types) {
+ if (types == null) {
+ return null;
+ }
+ return TextUtils.join(",", types);
+ }
+
+ private String nullToEmpty(String stringValue) {
+ return stringValue == null ? "" : stringValue;
+ }
+
+ /** @hide */
+ // Called by DPM.
+ public ContentValues toContentValues() {
+ ContentValues apnValue = new ContentValues();
+ apnValue.put(Telephony.Carriers.NUMERIC, nullToEmpty(mOperatorNumeric));
+ apnValue.put(Telephony.Carriers.NAME, nullToEmpty(mEntryName));
+ apnValue.put(Telephony.Carriers.APN, nullToEmpty(mApnName));
+ apnValue.put(Telephony.Carriers.PROXY, mProxy == null ? "" : inetAddressToString(mProxy));
+ apnValue.put(Telephony.Carriers.PORT, portToString(mPort));
+ apnValue.put(Telephony.Carriers.MMSC, mMmsc == null ? "" : URLToString(mMmsc));
+ apnValue.put(Telephony.Carriers.MMSPORT, portToString(mMmsPort));
+ apnValue.put(Telephony.Carriers.MMSPROXY, mMmsProxy == null
+ ? "" : inetAddressToString(mMmsProxy));
+ apnValue.put(Telephony.Carriers.USER, nullToEmpty(mUser));
+ apnValue.put(Telephony.Carriers.PASSWORD, nullToEmpty(mPassword));
+ apnValue.put(Telephony.Carriers.AUTH_TYPE, mAuthType);
+ String apnType = deParseTypes(mTypes);
+ apnValue.put(Telephony.Carriers.TYPE, nullToEmpty(apnType));
+ apnValue.put(Telephony.Carriers.PROTOCOL, nullToEmpty(mProtocol));
+ apnValue.put(Telephony.Carriers.ROAMING_PROTOCOL, nullToEmpty(mRoamingProtocol));
+ apnValue.put(Telephony.Carriers.CARRIER_ENABLED, mCarrierEnabled);
+ apnValue.put(Telephony.Carriers.MVNO_TYPE, nullToEmpty(mMvnoType));
+ apnValue.put(Telephony.Carriers.NETWORK_TYPE_BITMASK, mNetworkTypeBitmask);
+
+ return apnValue;
+ }
+
+ /**
+ * @param types comma delimited list of APN types
+ * @return array of APN types
+ * @hide
+ */
+ public static String[] parseTypes(String types) {
+ String[] result;
+ // If unset, set to DEFAULT.
+ if (TextUtils.isEmpty(types)) {
+ result = new String[1];
+ result[0] = TYPE_ALL;
+ } else {
+ result = types.split(",");
+ }
+ return result;
+ }
+
+ private static URL URLFromString(String url) {
+ try {
+ return TextUtils.isEmpty(url) ? null : new URL(url);
+ } catch (MalformedURLException e) {
+ Log.e(LOG_TAG, "Can't parse URL from string.");
+ return null;
+ }
+ }
+
+ private static String URLToString(URL url) {
+ return url == null ? "" : url.toString();
+ }
+
+ private static InetAddress inetAddressFromString(String inetAddress) {
+ if (TextUtils.isEmpty(inetAddress)) {
+ return null;
+ }
+ try {
+ return InetAddress.getByName(inetAddress);
+ } catch (UnknownHostException e) {
+ Log.e(LOG_TAG, "Can't parse InetAddress from string: unknown host.");
+ return null;
+ }
+ }
+
+ private static String inetAddressToString(InetAddress inetAddress) {
+ if (inetAddress == null) {
+ return null;
+ }
+ final String inetAddressString = inetAddress.toString();
+ if (TextUtils.isEmpty(inetAddressString)) {
+ return null;
+ }
+ final String hostName = inetAddressString.substring(0, inetAddressString.indexOf("/"));
+ final String address = inetAddressString.substring(inetAddressString.indexOf("/") + 1);
+ if (TextUtils.isEmpty(hostName) && TextUtils.isEmpty(address)) {
+ return null;
+ }
+ return TextUtils.isEmpty(hostName) ? address : hostName;
+ }
+
+ private static int portFromString(String strPort) {
+ int port = -1;
+ if (!TextUtils.isEmpty(strPort)) {
+ try {
+ port = Integer.parseInt(strPort);
+ } catch (NumberFormatException e) {
+ Log.e(LOG_TAG, "Can't parse port from String");
+ }
+ }
+ return port;
+ }
+
+ private static String portToString(int port) {
+ return port == -1 ? "" : Integer.toString(port);
+ }
+
+ // Implement Parcelable.
+ @Override
+ /** @hide */
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ /** @hide */
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mId);
+ dest.writeString(mOperatorNumeric);
+ dest.writeString(mEntryName);
+ dest.writeString(mApnName);
+ dest.writeValue(mProxy);
+ dest.writeInt(mPort);
+ dest.writeValue(mMmsc);
+ dest.writeValue(mMmsProxy);
+ dest.writeInt(mMmsPort);
+ dest.writeString(mUser);
+ dest.writeString(mPassword);
+ dest.writeInt(mAuthType);
+ dest.writeStringArray(mTypes.toArray(new String[0]));
+ dest.writeString(mProtocol);
+ dest.writeString(mRoamingProtocol);
+ dest.writeInt(mCarrierEnabled ? 1: 0);
+ dest.writeString(mMvnoType);
+ dest.writeInt(mNetworkTypeBitmask);
+ }
+
+ private static ApnSetting readFromParcel(Parcel in) {
+ final int id = in.readInt();
+ final String operatorNumeric = in.readString();
+ final String entryName = in.readString();
+ final String apnName = in.readString();
+ final InetAddress proxy = (InetAddress)in.readValue(InetAddress.class.getClassLoader());
+ final int port = in.readInt();
+ final URL mmsc = (URL)in.readValue(URL.class.getClassLoader());
+ final InetAddress mmsProxy = (InetAddress)in.readValue(InetAddress.class.getClassLoader());
+ final int mmsPort = in.readInt();
+ final String user = in.readString();
+ final String password = in.readString();
+ final int authType = in.readInt();
+ final List<String> types = Arrays.asList(in.readStringArray());
+ final String protocol = in.readString();
+ final String roamingProtocol = in.readString();
+ final boolean carrierEnabled = in.readInt() > 0;
+ final String mvnoType = in.readString();
+ final int networkTypeBitmask = in.readInt();
+
+ return makeApnSetting(id, operatorNumeric, entryName, apnName,
+ proxy, port, mmsc, mmsProxy, mmsPort, user, password, authType, types, protocol,
+ roamingProtocol, carrierEnabled, networkTypeBitmask, 0, false,
+ 0, 0, 0, 0, mvnoType, null);
+ }
+
+ public static final Parcelable.Creator<ApnSetting> CREATOR =
+ new Parcelable.Creator<ApnSetting>() {
+ @Override
+ public ApnSetting createFromParcel(Parcel in) {
+ return readFromParcel(in);
+ }
+
+ @Override
+ public ApnSetting[] newArray(int size) {
+ return new ApnSetting[size];
+ }
+ };
+
+ /**
+ * APN types for data connections. These are usage categories for an APN
+ * entry. One APN entry may support multiple APN types, eg, a single APN
+ * may service regular internet traffic ("default") as well as MMS-specific
+ * connections.<br/>
+ * ALL is a special type to indicate that this APN entry can
+ * service all data connections.
+ */
+ public static final String TYPE_ALL = "*";
+ /** APN type for default data traffic */
+ public static final String TYPE_DEFAULT = "default";
+ /** APN type for MMS traffic */
+ public static final String TYPE_MMS = "mms";
+ /** APN type for SUPL assisted GPS */
+ public static final String TYPE_SUPL = "supl";
+ /** APN type for DUN traffic */
+ public static final String TYPE_DUN = "dun";
+ /** APN type for HiPri traffic */
+ public static final String TYPE_HIPRI = "hipri";
+ /** APN type for FOTA */
+ public static final String TYPE_FOTA = "fota";
+ /** APN type for IMS */
+ public static final String TYPE_IMS = "ims";
+ /** APN type for CBS */
+ public static final String TYPE_CBS = "cbs";
+ /** APN type for IA Initial Attach APN */
+ public static final String TYPE_IA = "ia";
+ /** APN type for Emergency PDN. This is not an IA apn, but is used
+ * for access to carrier services in an emergency call situation. */
+ public static final String TYPE_EMERGENCY = "emergency";
+ /**
+ * Array of all APN types
+ *
+ * @hide
+ */
+ public static final String[] ALL_TYPES = {
+ TYPE_DEFAULT,
+ TYPE_MMS,
+ TYPE_SUPL,
+ TYPE_DUN,
+ TYPE_HIPRI,
+ TYPE_FOTA,
+ TYPE_IMS,
+ TYPE_CBS,
+ TYPE_IA,
+ TYPE_EMERGENCY
+ };
+
+ // Possible values for authentication types.
+ public static final int AUTH_TYPE_NONE = 0;
+ public static final int AUTH_TYPE_PAP = 1;
+ public static final int AUTH_TYPE_CHAP = 2;
+ public static final int AUTH_TYPE_PAP_OR_CHAP = 3;
+
+ // Possible values for protocol.
+ public static final String PROTOCOL_IP = "IP";
+ public static final String PROTOCOL_IPV6 = "IPV6";
+ public static final String PROTOCOL_IPV4V6 = "IPV4V6";
+ public static final String PROTOCOL_PPP = "PPP";
+
+ // Possible values for MVNO type.
+ public static final String MVNO_TYPE_SPN = "spn";
+ public static final String MVNO_TYPE_IMSI = "imsi";
+ public static final String MVNO_TYPE_GID = "gid";
+ public static final String MVNO_TYPE_ICCID = "iccid";
+
+ public static class Builder{
+ private String mEntryName;
+ private String mApnName;
+ private InetAddress mProxy;
+ private int mPort = -1;
+ private URL mMmsc;
+ private InetAddress mMmsProxy;
+ private int mMmsPort = -1;
+ private String mUser;
+ private String mPassword;
+ private int mAuthType;
+ private List<String> mTypes;
+ private int mTypesBitmap;
+ private int mId;
+ private String mOperatorNumeric;
+ private String mProtocol;
+ private String mRoamingProtocol;
+ private int mMtu;
+ private int mNetworkTypeBitmask;
+ private boolean mCarrierEnabled;
+ private int mProfileId;
+ private boolean mModemCognitive;
+ private int mMaxConns;
+ private int mWaitTime;
+ private int mMaxConnsTime;
+ private String mMvnoType;
+ private String mMvnoMatchData;
+
+ /**
+ * Default constructor for Builder.
+ */
+ public Builder() {}
+
+ /**
+ * Sets the unique database id for this entry.
+ *
+ * @param id the unique database id to set for this entry
+ */
+ private Builder setId(int id) {
+ this.mId = id;
+ return this;
+ }
+
+ /**
+ * Set the MTU size of the mobile interface to which the APN connected.
+ *
+ * @param mtu the MTU size to set for the APN
+ * @hide
+ */
+ public Builder setMtu(int mtu) {
+ this.mMtu = mtu;
+ return this;
+ }
+
+ /**
+ * Sets the profile id to which the APN saved in modem.
+ *
+ * @param profileId the profile id to set for the APN
+ * @hide
+ */
+ public Builder setProfileId(int profileId) {
+ this.mProfileId = profileId;
+ return this;
+ }
+
+ /**
+ * Sets if the APN setting is to be set in modem.
+ *
+ * @param modemCognitive if the APN setting is to be set in modem
+ * @hide
+ */
+ public Builder setModemCognitive(boolean modemCognitive) {
+ this.mModemCognitive = modemCognitive;
+ return this;
+ }
+
+ /**
+ * Sets the max connections of this APN.
+ *
+ * @param maxConns the max connections of this APN
+ * @hide
+ */
+ public Builder setMaxConns(int maxConns) {
+ this.mMaxConns = maxConns;
+ return this;
+ }
+
+ /**
+ * Sets the wait time for retry of the APN.
+ *
+ * @param waitTime the wait time for retry of the APN
+ * @hide
+ */
+ public Builder setWaitTime(int waitTime) {
+ this.mWaitTime = waitTime;
+ return this;
+ }
+
+ /**
+ * Sets the time to limit max connection for the APN.
+ *
+ * @param maxConnsTime the time to limit max connection for the APN
+ * @hide
+ */
+ public Builder setMaxConnsTime(int maxConnsTime) {
+ this.mMaxConnsTime = maxConnsTime;
+ return this;
+ }
+
+ /**
+ * Sets the MVNO match data for the APN.
+ *
+ * @param mvnoMatchData the MVNO match data for the APN
+ * @hide
+ */
+ public Builder setMvnoMatchData(String mvnoMatchData) {
+ this.mMvnoMatchData = mvnoMatchData;
+ return this;
+ }
+
+ /**
+ * Sets the entry name of the APN.
+ *
+ * @param entryName the entry name to set for the APN
+ */
+ public Builder setEntryName(String entryName) {
+ this.mEntryName = entryName;
+ return this;
+ }
+
+ /**
+ * Sets the name of the APN.
+ *
+ * @param apnName the name to set for the APN
+ */
+ public Builder setApnName(String apnName) {
+ this.mApnName = apnName;
+ return this;
+ }
+
+ /**
+ * Sets the proxy address of the APN.
+ *
+ * @param proxy the proxy address to set for the APN
+ */
+ public Builder setProxy(InetAddress proxy) {
+ this.mProxy = proxy;
+ return this;
+ }
+
+ /**
+ * Sets the proxy port of the APN.
+ *
+ * @param port the proxy port to set for the APN
+ */
+ public Builder setPort(int port) {
+ this.mPort = port;
+ return this;
+ }
+
+ /**
+ * Sets the MMSC URL of the APN.
+ *
+ * @param mmsc the MMSC URL to set for the APN
+ */
+ public Builder setMmsc(URL mmsc) {
+ this.mMmsc = mmsc;
+ return this;
+ }
+
+ /**
+ * Sets the MMS proxy address of the APN.
+ *
+ * @param mmsProxy the MMS proxy address to set for the APN
+ */
+ public Builder setMmsProxy(InetAddress mmsProxy) {
+ this.mMmsProxy = mmsProxy;
+ return this;
+ }
+
+ /**
+ * Sets the MMS proxy port of the APN.
+ *
+ * @param mmsPort the MMS proxy port to set for the APN
+ */
+ public Builder setMmsPort(int mmsPort) {
+ this.mMmsPort = mmsPort;
+ return this;
+ }
+
+ /**
+ * Sets the APN username of the APN.
+ *
+ * @param user the APN username to set for the APN
+ */
+ public Builder setUser(String user) {
+ this.mUser = user;
+ return this;
+ }
+
+ /**
+ * Sets the APN password of the APN.
+ *
+ * @see android.provider.Telephony.Carriers#PASSWORD
+ * @param password the APN password to set for the APN
+ */
+ public Builder setPassword(String password) {
+ this.mPassword = password;
+ return this;
+ }
+
+ /**
+ * Sets the authentication type of the APN.
+ *
+ * Example of possible values: {@link #AUTH_TYPE_NONE}, {@link #AUTH_TYPE_PAP}.
+ *
+ * @param authType the authentication type to set for the APN
+ */
+ public Builder setAuthType(@AuthType int authType) {
+ this.mAuthType = authType;
+ return this;
+ }
+
+ /**
+ * Sets the list of APN types of the APN.
+ *
+ * Example of possible values: {@link #TYPE_DEFAULT}, {@link #TYPE_MMS}.
+ *
+ * @param types the list of APN types to set for the APN
+ */
+ public Builder setTypes(@ApnType List<String> types) {
+ this.mTypes = types;
+ int apnBitmap = 0;
+ for (int i = 0; i < mTypes.size(); i++) {
+ mTypes.set(i, mTypes.get(i).toLowerCase());
+ apnBitmap |= getApnBitmask(mTypes.get(i));
+ }
+ this.mTypesBitmap = apnBitmap;
+ return this;
+ }
+
+ /**
+ * Set the numeric operator ID for the APN.
+ *
+ * @param operatorNumeric the numeric operator ID to set for this entry
+ */
+ public Builder setOperatorNumeric(String operatorNumeric) {
+ this.mOperatorNumeric = operatorNumeric;
+ return this;
+ }
+
+ /**
+ * Sets the protocol to use to connect to this APN.
+ *
+ * One of the {@code PDP_type} values in TS 27.007 section 10.1.1.
+ * Example of possible values: {@link #PROTOCOL_IP}, {@link #PROTOCOL_IPV6}.
+ *
+ * @param protocol the protocol to set to use to connect to this APN
+ */
+ public Builder setProtocol(@ProtocolType String protocol) {
+ this.mProtocol = protocol;
+ return this;
+ }
+
+ /**
+ * Sets the protocol to use to connect to this APN when roaming.
+ *
+ * @param roamingProtocol the protocol to set to use to connect to this APN when roaming
+ */
+ public Builder setRoamingProtocol(String roamingProtocol) {
+ this.mRoamingProtocol = roamingProtocol;
+ return this;
+ }
+
+ /**
+ * Sets the current status for this APN.
+ *
+ * @param carrierEnabled the current status to set for this APN
+ */
+ public Builder setCarrierEnabled(boolean carrierEnabled) {
+ this.mCarrierEnabled = carrierEnabled;
+ return this;
+ }
+
+ /**
+ * Sets Radio Technology (Network Type) info for this APN.
+ *
+ * @param networkTypeBitmask the Radio Technology (Network Type) info
+ */
+ public Builder setNetworkTypeBitmask(int networkTypeBitmask) {
+ this.mNetworkTypeBitmask = networkTypeBitmask;
+ return this;
+ }
+
+ /**
+ * Sets the MVNO match type for this APN.
+ *
+ * Example of possible values: {@link #MVNO_TYPE_SPN}, {@link #MVNO_TYPE_IMSI}.
+ *
+ * @param mvnoType the MVNO match type to set for this APN
+ */
+ public Builder setMvnoType(@MvnoType String mvnoType) {
+ this.mMvnoType = mvnoType;
+ return this;
+ }
+
+ public ApnSetting build() {
+ return new ApnSetting(this);
+ }
+ }
+}
+
diff --git a/android/telephony/data/DataCallResponse.java b/android/telephony/data/DataCallResponse.java
index da51c86..ef3a183 100644
--- a/android/telephony/data/DataCallResponse.java
+++ b/android/telephony/data/DataCallResponse.java
@@ -20,6 +20,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.net.LinkAddress;
import android.os.Parcel;
import android.os.Parcelable;
@@ -40,7 +41,7 @@
private final int mActive;
private final String mType;
private final String mIfname;
- private final List<InterfaceAddress> mAddresses;
+ private final List<LinkAddress> mAddresses;
private final List<InetAddress> mDnses;
private final List<InetAddress> mGateways;
private final List<String> mPcscfs;
@@ -71,7 +72,7 @@
*/
public DataCallResponse(int status, int suggestedRetryTime, int cid, int active,
@Nullable String type, @Nullable String ifname,
- @Nullable List<InterfaceAddress> addresses,
+ @Nullable List<LinkAddress> addresses,
@Nullable List<InetAddress> dnses,
@Nullable List<InetAddress> gateways,
@Nullable List<String> pcscfs, int mtu) {
@@ -96,7 +97,7 @@
mType = source.readString();
mIfname = source.readString();
mAddresses = new ArrayList<>();
- source.readList(mAddresses, InterfaceAddress.class.getClassLoader());
+ source.readList(mAddresses, LinkAddress.class.getClassLoader());
mDnses = new ArrayList<>();
source.readList(mDnses, InetAddress.class.getClassLoader());
mGateways = new ArrayList<>();
@@ -140,10 +141,10 @@
public String getIfname() { return mIfname; }
/**
- * @return A list of {@link InterfaceAddress}
+ * @return A list of {@link LinkAddress}
*/
@NonNull
- public List<InterfaceAddress> getAddresses() { return mAddresses; }
+ public List<LinkAddress> getAddresses() { return mAddresses; }
/**
* @return A list of DNS server addresses, e.g., "192.0.1.3" or
diff --git a/android/telephony/data/DataService.java b/android/telephony/data/DataService.java
new file mode 100644
index 0000000..fa19ea0
--- /dev/null
+++ b/android/telephony/data/DataService.java
@@ -0,0 +1,567 @@
+/*
+ * Copyright 2017 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.telephony.data;
+
+import android.annotation.CallSuper;
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.net.LinkProperties;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.Rlog;
+import android.telephony.SubscriptionManager;
+import android.util.SparseArray;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base class of data service. Services that extend DataService must register the service in
+ * their AndroidManifest to be detected by the framework. They must be protected by the permission
+ * "android.permission.BIND_DATA_SERVICE". The data service definition in the manifest must follow
+ * the following format:
+ * ...
+ * <service android:name=".xxxDataService"
+ * android:permission="android.permission.BIND_DATA_SERVICE" >
+ * <intent-filter>
+ * <action android:name="android.telephony.data.DataService" />
+ * </intent-filter>
+ * </service>
+ * @hide
+ */
+@SystemApi
+public abstract class DataService extends Service {
+ private static final String TAG = DataService.class.getSimpleName();
+
+ public static final String DATA_SERVICE_INTERFACE = "android.telephony.data.DataService";
+ public static final String DATA_SERVICE_EXTRA_SLOT_ID = "android.telephony.data.extra.SLOT_ID";
+
+ /** {@hide} */
+ @IntDef(prefix = "REQUEST_REASON_", value = {
+ REQUEST_REASON_NORMAL,
+ REQUEST_REASON_HANDOVER,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SetupDataReason {}
+
+ /** {@hide} */
+ @IntDef(prefix = "REQUEST_REASON_", value = {
+ REQUEST_REASON_NORMAL,
+ REQUEST_REASON_SHUTDOWN,
+ REQUEST_REASON_HANDOVER,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DeactivateDataReason {}
+
+
+ /** The reason of the data request is normal */
+ public static final int REQUEST_REASON_NORMAL = 1;
+
+ /** The reason of the data request is device shutdown */
+ public static final int REQUEST_REASON_SHUTDOWN = 2;
+
+ /** The reason of the data request is IWLAN handover */
+ public static final int REQUEST_REASON_HANDOVER = 3;
+
+ private static final int DATA_SERVICE_INTERNAL_REQUEST_INITIALIZE_SERVICE = 1;
+ private static final int DATA_SERVICE_REQUEST_SETUP_DATA_CALL = 2;
+ private static final int DATA_SERVICE_REQUEST_DEACTIVATE_DATA_CALL = 3;
+ private static final int DATA_SERVICE_REQUEST_SET_INITIAL_ATTACH_APN = 4;
+ private static final int DATA_SERVICE_REQUEST_SET_DATA_PROFILE = 5;
+ private static final int DATA_SERVICE_REQUEST_GET_DATA_CALL_LIST = 6;
+ private static final int DATA_SERVICE_REQUEST_REGISTER_DATA_CALL_LIST_CHANGED = 7;
+ private static final int DATA_SERVICE_REQUEST_UNREGISTER_DATA_CALL_LIST_CHANGED = 8;
+ private static final int DATA_SERVICE_INDICATION_DATA_CALL_LIST_CHANGED = 9;
+
+ private final HandlerThread mHandlerThread;
+
+ private final DataServiceHandler mHandler;
+
+ private final SparseArray<DataServiceProvider> mServiceMap = new SparseArray<>();
+
+ private final SparseArray<IDataServiceWrapper> mBinderMap = new SparseArray<>();
+
+ /**
+ * The abstract class of the actual data service implementation. The data service provider
+ * must extend this class to support data connection. Note that each instance of data service
+ * provider is associated with one physical SIM slot.
+ */
+ public class DataServiceProvider {
+
+ private final int mSlotId;
+
+ private final List<IDataServiceCallback> mDataCallListChangedCallbacks = new ArrayList<>();
+
+ /**
+ * Constructor
+ * @param slotId SIM slot id the data service provider associated with.
+ */
+ public DataServiceProvider(int slotId) {
+ mSlotId = slotId;
+ }
+
+ /**
+ * @return SIM slot id the data service provider associated with.
+ */
+ public final int getSlotId() {
+ return mSlotId;
+ }
+
+ /**
+ * Setup a data connection. The data service provider must implement this method to support
+ * establishing a packet data connection. When completed or error, the service must invoke
+ * the provided callback to notify the platform.
+ *
+ * @param accessNetworkType Access network type that the data call will be established on.
+ * Must be one of {@link AccessNetworkConstants.AccessNetworkType}.
+ * @param dataProfile Data profile used for data call setup. See {@link DataProfile}
+ * @param isRoaming True if the device is data roaming.
+ * @param allowRoaming True if data roaming is allowed by the user.
+ * @param reason The reason for data setup. Must be {@link #REQUEST_REASON_NORMAL} or
+ * {@link #REQUEST_REASON_HANDOVER}.
+ * @param linkProperties If {@code reason} is {@link #REQUEST_REASON_HANDOVER}, this is the
+ * link properties of the existing data connection, otherwise null.
+ * @param callback The result callback for this request.
+ */
+ public void setupDataCall(int accessNetworkType, DataProfile dataProfile, boolean isRoaming,
+ boolean allowRoaming, @SetupDataReason int reason,
+ LinkProperties linkProperties, DataServiceCallback callback) {
+ // The default implementation is to return unsupported.
+ callback.onSetupDataCallComplete(DataServiceCallback.RESULT_ERROR_UNSUPPORTED, null);
+ }
+
+ /**
+ * Deactivate a data connection. The data service provider must implement this method to
+ * support data connection tear down. When completed or error, the service must invoke the
+ * provided callback to notify the platform.
+ *
+ * @param cid Call id returned in the callback of {@link DataServiceProvider#setupDataCall(
+ * int, DataProfile, boolean, boolean, int, LinkProperties, DataServiceCallback)}.
+ * @param reason The reason for data deactivation. Must be {@link #REQUEST_REASON_NORMAL},
+ * {@link #REQUEST_REASON_SHUTDOWN} or {@link #REQUEST_REASON_HANDOVER}.
+ * @param callback The result callback for this request.
+ */
+ public void deactivateDataCall(int cid, @DeactivateDataReason int reason,
+ DataServiceCallback callback) {
+ // The default implementation is to return unsupported.
+ callback.onDeactivateDataCallComplete(DataServiceCallback.RESULT_ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Set an APN to initial attach network.
+ *
+ * @param dataProfile Data profile used for data call setup. See {@link DataProfile}.
+ * @param isRoaming True if the device is data roaming.
+ * @param callback The result callback for this request.
+ */
+ public void setInitialAttachApn(DataProfile dataProfile, boolean isRoaming,
+ DataServiceCallback callback) {
+ // The default implementation is to return unsupported.
+ callback.onSetInitialAttachApnComplete(DataServiceCallback.RESULT_ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Send current carrier's data profiles to the data service for data call setup. This is
+ * only for CDMA carrier that can change the profile through OTA. The data service should
+ * always uses the latest data profile sent by the framework.
+ *
+ * @param dps A list of data profiles.
+ * @param isRoaming True if the device is data roaming.
+ * @param callback The result callback for this request.
+ */
+ public void setDataProfile(List<DataProfile> dps, boolean isRoaming,
+ DataServiceCallback callback) {
+ // The default implementation is to return unsupported.
+ callback.onSetDataProfileComplete(DataServiceCallback.RESULT_ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Get the active data call list.
+ *
+ * @param callback The result callback for this request.
+ */
+ public void getDataCallList(DataServiceCallback callback) {
+ // The default implementation is to return unsupported.
+ callback.onGetDataCallListComplete(DataServiceCallback.RESULT_ERROR_UNSUPPORTED, null);
+ }
+
+ private void registerForDataCallListChanged(IDataServiceCallback callback) {
+ synchronized (mDataCallListChangedCallbacks) {
+ mDataCallListChangedCallbacks.add(callback);
+ }
+ }
+
+ private void unregisterForDataCallListChanged(IDataServiceCallback callback) {
+ synchronized (mDataCallListChangedCallbacks) {
+ mDataCallListChangedCallbacks.remove(callback);
+ }
+ }
+
+ /**
+ * Notify the system that current data call list changed. Data service must invoke this
+ * method whenever there is any data call status changed.
+ *
+ * @param dataCallList List of the current active data call.
+ */
+ public final void notifyDataCallListChanged(List<DataCallResponse> dataCallList) {
+ synchronized (mDataCallListChangedCallbacks) {
+ for (IDataServiceCallback callback : mDataCallListChangedCallbacks) {
+ mHandler.obtainMessage(DATA_SERVICE_INDICATION_DATA_CALL_LIST_CHANGED, mSlotId,
+ 0, new DataCallListChangedIndication(dataCallList, callback))
+ .sendToTarget();
+ }
+ }
+ }
+
+ /**
+ * Called when the instance of data service is destroyed (e.g. got unbind or binder died).
+ */
+ @CallSuper
+ protected void onDestroy() {
+ mDataCallListChangedCallbacks.clear();
+ }
+ }
+
+ private static final class SetupDataCallRequest {
+ public final int accessNetworkType;
+ public final DataProfile dataProfile;
+ public final boolean isRoaming;
+ public final boolean allowRoaming;
+ public final int reason;
+ public final LinkProperties linkProperties;
+ public final IDataServiceCallback callback;
+ SetupDataCallRequest(int accessNetworkType, DataProfile dataProfile, boolean isRoaming,
+ boolean allowRoaming, int reason, LinkProperties linkProperties,
+ IDataServiceCallback callback) {
+ this.accessNetworkType = accessNetworkType;
+ this.dataProfile = dataProfile;
+ this.isRoaming = isRoaming;
+ this.allowRoaming = allowRoaming;
+ this.linkProperties = linkProperties;
+ this.reason = reason;
+ this.callback = callback;
+ }
+ }
+
+ private static final class DeactivateDataCallRequest {
+ public final int cid;
+ public final int reason;
+ public final IDataServiceCallback callback;
+ DeactivateDataCallRequest(int cid, int reason, IDataServiceCallback callback) {
+ this.cid = cid;
+ this.reason = reason;
+ this.callback = callback;
+ }
+ }
+
+ private static final class SetInitialAttachApnRequest {
+ public final DataProfile dataProfile;
+ public final boolean isRoaming;
+ public final IDataServiceCallback callback;
+ SetInitialAttachApnRequest(DataProfile dataProfile, boolean isRoaming,
+ IDataServiceCallback callback) {
+ this.dataProfile = dataProfile;
+ this.isRoaming = isRoaming;
+ this.callback = callback;
+ }
+ }
+
+ private static final class SetDataProfileRequest {
+ public final List<DataProfile> dps;
+ public final boolean isRoaming;
+ public final IDataServiceCallback callback;
+ SetDataProfileRequest(List<DataProfile> dps, boolean isRoaming,
+ IDataServiceCallback callback) {
+ this.dps = dps;
+ this.isRoaming = isRoaming;
+ this.callback = callback;
+ }
+ }
+
+ private static final class DataCallListChangedIndication {
+ public final List<DataCallResponse> dataCallList;
+ public final IDataServiceCallback callback;
+ DataCallListChangedIndication(List<DataCallResponse> dataCallList,
+ IDataServiceCallback callback) {
+ this.dataCallList = dataCallList;
+ this.callback = callback;
+ }
+ }
+
+ private class DataServiceHandler extends Handler {
+
+ DataServiceHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ IDataServiceCallback callback;
+ final int slotId = message.arg1;
+ DataServiceProvider service;
+
+ synchronized (mServiceMap) {
+ service = mServiceMap.get(slotId);
+ }
+
+ switch (message.what) {
+ case DATA_SERVICE_INTERNAL_REQUEST_INITIALIZE_SERVICE:
+ service = createDataServiceProvider(message.arg1);
+ if (service != null) {
+ mServiceMap.put(slotId, service);
+ }
+ break;
+ case DATA_SERVICE_REQUEST_SETUP_DATA_CALL:
+ if (service == null) break;
+ SetupDataCallRequest setupDataCallRequest = (SetupDataCallRequest) message.obj;
+ service.setupDataCall(setupDataCallRequest.accessNetworkType,
+ setupDataCallRequest.dataProfile, setupDataCallRequest.isRoaming,
+ setupDataCallRequest.allowRoaming, setupDataCallRequest.reason,
+ setupDataCallRequest.linkProperties,
+ new DataServiceCallback(setupDataCallRequest.callback));
+
+ break;
+ case DATA_SERVICE_REQUEST_DEACTIVATE_DATA_CALL:
+ if (service == null) break;
+ DeactivateDataCallRequest deactivateDataCallRequest =
+ (DeactivateDataCallRequest) message.obj;
+ service.deactivateDataCall(deactivateDataCallRequest.cid,
+ deactivateDataCallRequest.reason,
+ new DataServiceCallback(deactivateDataCallRequest.callback));
+ break;
+ case DATA_SERVICE_REQUEST_SET_INITIAL_ATTACH_APN:
+ if (service == null) break;
+ SetInitialAttachApnRequest setInitialAttachApnRequest =
+ (SetInitialAttachApnRequest) message.obj;
+ service.setInitialAttachApn(setInitialAttachApnRequest.dataProfile,
+ setInitialAttachApnRequest.isRoaming,
+ new DataServiceCallback(setInitialAttachApnRequest.callback));
+ break;
+ case DATA_SERVICE_REQUEST_SET_DATA_PROFILE:
+ if (service == null) break;
+ SetDataProfileRequest setDataProfileRequest =
+ (SetDataProfileRequest) message.obj;
+ service.setDataProfile(setDataProfileRequest.dps,
+ setDataProfileRequest.isRoaming,
+ new DataServiceCallback(setDataProfileRequest.callback));
+ break;
+ case DATA_SERVICE_REQUEST_GET_DATA_CALL_LIST:
+ if (service == null) break;
+
+ service.getDataCallList(new DataServiceCallback(
+ (IDataServiceCallback) message.obj));
+ break;
+ case DATA_SERVICE_REQUEST_REGISTER_DATA_CALL_LIST_CHANGED:
+ if (service == null) break;
+ service.registerForDataCallListChanged((IDataServiceCallback) message.obj);
+ break;
+ case DATA_SERVICE_REQUEST_UNREGISTER_DATA_CALL_LIST_CHANGED:
+ if (service == null) break;
+ callback = (IDataServiceCallback) message.obj;
+ service.unregisterForDataCallListChanged(callback);
+ break;
+ case DATA_SERVICE_INDICATION_DATA_CALL_LIST_CHANGED:
+ if (service == null) break;
+ DataCallListChangedIndication indication =
+ (DataCallListChangedIndication) message.obj;
+ try {
+ indication.callback.onDataCallListChanged(indication.dataCallList);
+ } catch (RemoteException e) {
+ loge("Failed to call onDataCallListChanged. " + e);
+ }
+ break;
+ }
+ }
+ }
+
+ /** @hide */
+ protected DataService() {
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+
+ mHandler = new DataServiceHandler(mHandlerThread.getLooper());
+ log("Data service created");
+ }
+
+ /**
+ * Create the instance of {@link DataServiceProvider}. Data service provider must override
+ * this method to facilitate the creation of {@link DataServiceProvider} instances. The system
+ * will call this method after binding the data service for each active SIM slot id.
+ *
+ * @param slotId SIM slot id the data service associated with.
+ * @return Data service object
+ */
+ public abstract DataServiceProvider createDataServiceProvider(int slotId);
+
+ /** @hide */
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (intent == null || !DATA_SERVICE_INTERFACE.equals(intent.getAction())) {
+ loge("Unexpected intent " + intent);
+ return null;
+ }
+
+ int slotId = intent.getIntExtra(
+ DATA_SERVICE_EXTRA_SLOT_ID, SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+
+ if (!SubscriptionManager.isValidSlotIndex(slotId)) {
+ loge("Invalid slot id " + slotId);
+ return null;
+ }
+
+ log("onBind: slot id=" + slotId);
+
+ IDataServiceWrapper binder = mBinderMap.get(slotId);
+ if (binder == null) {
+ Message msg = mHandler.obtainMessage(DATA_SERVICE_INTERNAL_REQUEST_INITIALIZE_SERVICE);
+ msg.arg1 = slotId;
+ msg.sendToTarget();
+
+ binder = new IDataServiceWrapper(slotId);
+ mBinderMap.put(slotId, binder);
+ }
+
+ return binder;
+ }
+
+ /** @hide */
+ @Override
+ public boolean onUnbind(Intent intent) {
+ int slotId = intent.getIntExtra(DATA_SERVICE_EXTRA_SLOT_ID,
+ SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+ if (mBinderMap.get(slotId) != null) {
+ DataServiceProvider serviceImpl;
+ synchronized (mServiceMap) {
+ serviceImpl = mServiceMap.get(slotId);
+ }
+ if (serviceImpl != null) {
+ serviceImpl.onDestroy();
+ }
+ mBinderMap.remove(slotId);
+ }
+
+ // If all clients unbinds, quit the handler thread
+ if (mBinderMap.size() == 0) {
+ mHandlerThread.quit();
+ }
+
+ return false;
+ }
+
+ /** @hide */
+ @Override
+ public void onDestroy() {
+ synchronized (mServiceMap) {
+ for (int i = 0; i < mServiceMap.size(); i++) {
+ DataServiceProvider serviceImpl = mServiceMap.get(i);
+ if (serviceImpl != null) {
+ serviceImpl.onDestroy();
+ }
+ }
+ mServiceMap.clear();
+ }
+
+ mHandlerThread.quit();
+ }
+
+ /**
+ * A wrapper around IDataService that forwards calls to implementations of {@link DataService}.
+ */
+ private class IDataServiceWrapper extends IDataService.Stub {
+
+ private final int mSlotId;
+
+ IDataServiceWrapper(int slotId) {
+ mSlotId = slotId;
+ }
+
+ @Override
+ public void setupDataCall(int accessNetworkType, DataProfile dataProfile,
+ boolean isRoaming, boolean allowRoaming, int reason,
+ LinkProperties linkProperties, IDataServiceCallback callback) {
+ mHandler.obtainMessage(DATA_SERVICE_REQUEST_SETUP_DATA_CALL, mSlotId, 0,
+ new SetupDataCallRequest(accessNetworkType, dataProfile, isRoaming,
+ allowRoaming, reason, linkProperties, callback))
+ .sendToTarget();
+ }
+
+ @Override
+ public void deactivateDataCall(int cid, int reason, IDataServiceCallback callback) {
+ mHandler.obtainMessage(DATA_SERVICE_REQUEST_DEACTIVATE_DATA_CALL, mSlotId, 0,
+ new DeactivateDataCallRequest(cid, reason, callback))
+ .sendToTarget();
+ }
+
+ @Override
+ public void setInitialAttachApn(DataProfile dataProfile, boolean isRoaming,
+ IDataServiceCallback callback) {
+ mHandler.obtainMessage(DATA_SERVICE_REQUEST_SET_INITIAL_ATTACH_APN, mSlotId, 0,
+ new SetInitialAttachApnRequest(dataProfile, isRoaming, callback))
+ .sendToTarget();
+ }
+
+ @Override
+ public void setDataProfile(List<DataProfile> dps, boolean isRoaming,
+ IDataServiceCallback callback) {
+ mHandler.obtainMessage(DATA_SERVICE_REQUEST_SET_DATA_PROFILE, mSlotId, 0,
+ new SetDataProfileRequest(dps, isRoaming, callback)).sendToTarget();
+ }
+
+ @Override
+ public void getDataCallList(IDataServiceCallback callback) {
+ mHandler.obtainMessage(DATA_SERVICE_REQUEST_GET_DATA_CALL_LIST, mSlotId, 0,
+ callback).sendToTarget();
+ }
+
+ @Override
+ public void registerForDataCallListChanged(IDataServiceCallback callback) {
+ if (callback == null) {
+ loge("Callback is null");
+ return;
+ }
+ mHandler.obtainMessage(DATA_SERVICE_REQUEST_REGISTER_DATA_CALL_LIST_CHANGED, mSlotId,
+ 0, callback).sendToTarget();
+ }
+
+ @Override
+ public void unregisterForDataCallListChanged(IDataServiceCallback callback) {
+ if (callback == null) {
+ loge("Callback is null");
+ return;
+ }
+ mHandler.obtainMessage(DATA_SERVICE_REQUEST_UNREGISTER_DATA_CALL_LIST_CHANGED, mSlotId,
+ 0, callback).sendToTarget();
+ }
+ }
+
+ private void log(String s) {
+ Rlog.d(TAG, s);
+ }
+
+ private void loge(String s) {
+ Rlog.e(TAG, s);
+ }
+}
diff --git a/android/telephony/data/DataServiceCallback.java b/android/telephony/data/DataServiceCallback.java
new file mode 100644
index 0000000..b6a81f9
--- /dev/null
+++ b/android/telephony/data/DataServiceCallback.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2017 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.telephony.data;
+
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
+import android.os.RemoteException;
+import android.telephony.Rlog;
+import android.telephony.data.DataService.DataServiceProvider;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.List;
+
+/**
+ * Data service callback, which is for bound data service to invoke for solicited and unsolicited
+ * response. The caller is responsible to create a callback object for each single asynchronous
+ * request.
+ *
+ * @hide
+ */
+@SystemApi
+public class DataServiceCallback {
+
+ private static final String mTag = DataServiceCallback.class.getSimpleName();
+
+ /**
+ * Result of data requests
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({RESULT_SUCCESS, RESULT_ERROR_UNSUPPORTED, RESULT_ERROR_INVALID_ARG, RESULT_ERROR_BUSY,
+ RESULT_ERROR_ILLEGAL_STATE})
+ public @interface Result {}
+
+ /** Request is completed successfully */
+ public static final int RESULT_SUCCESS = 0;
+ /** Request is not support */
+ public static final int RESULT_ERROR_UNSUPPORTED = 1;
+ /** Request contains invalid arguments */
+ public static final int RESULT_ERROR_INVALID_ARG = 2;
+ /** Service is busy */
+ public static final int RESULT_ERROR_BUSY = 3;
+ /** Request sent in illegal state */
+ public static final int RESULT_ERROR_ILLEGAL_STATE = 4;
+
+ private final WeakReference<IDataServiceCallback> mCallback;
+
+ /** @hide */
+ public DataServiceCallback(IDataServiceCallback callback) {
+ mCallback = new WeakReference<>(callback);
+ }
+
+ /**
+ * Called to indicate result for the request {@link DataServiceProvider#setupDataCall(int,
+ * DataProfile, boolean, boolean, boolean, DataServiceCallback)}.
+ *
+ * @param result The result code. Must be one of the {@link Result}.
+ * @param response Setup data call response.
+ */
+ public void onSetupDataCallComplete(@Result int result, DataCallResponse response) {
+ IDataServiceCallback callback = mCallback.get();
+ if (callback != null) {
+ try {
+ callback.onSetupDataCallComplete(result, response);
+ } catch (RemoteException e) {
+ Rlog.e(mTag, "Failed to onSetupDataCallComplete on the remote");
+ }
+ }
+ }
+
+ /**
+ * Called to indicate result for the request {@link DataServiceProvider#deactivateDataCall(int,
+ * boolean, boolean, DataServiceCallback)}.
+ *
+ * @param result The result code. Must be one of the {@link Result}.
+ */
+ public void onDeactivateDataCallComplete(@Result int result) {
+ IDataServiceCallback callback = mCallback.get();
+ if (callback != null) {
+ try {
+ callback.onDeactivateDataCallComplete(result);
+ } catch (RemoteException e) {
+ Rlog.e(mTag, "Failed to onDeactivateDataCallComplete on the remote");
+ }
+ }
+ }
+
+ /**
+ * Called to indicate result for the request {@link DataServiceProvider#setInitialAttachApn(
+ * DataProfile, boolean, DataServiceCallback)}.
+ *
+ * @param result The result code. Must be one of the {@link Result}.
+ */
+ public void onSetInitialAttachApnComplete(@Result int result) {
+ IDataServiceCallback callback = mCallback.get();
+ if (callback != null) {
+ try {
+ callback.onSetInitialAttachApnComplete(result);
+ } catch (RemoteException e) {
+ Rlog.e(mTag, "Failed to onSetInitialAttachApnComplete on the remote");
+ }
+ }
+ }
+
+ /**
+ * Called to indicate result for the request {@link DataServiceProvider#setDataProfile(List,
+ * boolean, DataServiceCallback)}.
+ *
+ * @param result The result code. Must be one of the {@link Result}.
+ */
+ @SystemApi
+ public void onSetDataProfileComplete(@Result int result) {
+ IDataServiceCallback callback = mCallback.get();
+ if (callback != null) {
+ try {
+ callback.onSetDataProfileComplete(result);
+ } catch (RemoteException e) {
+ Rlog.e(mTag, "Failed to onSetDataProfileComplete on the remote");
+ }
+ }
+ }
+
+ /**
+ * Called to indicate result for the request {@link DataServiceProvider#getDataCallList(
+ * DataServiceCallback)}.
+ *
+ * @param result The result code. Must be one of the {@link Result}.
+ * @param dataCallList List of the current active data connection.
+ */
+ public void onGetDataCallListComplete(@Result int result, List<DataCallResponse> dataCallList) {
+ IDataServiceCallback callback = mCallback.get();
+ if (callback != null) {
+ try {
+ callback.onGetDataCallListComplete(result, dataCallList);
+ } catch (RemoteException e) {
+ Rlog.e(mTag, "Failed to onGetDataCallListComplete on the remote");
+ }
+ }
+ }
+
+ /**
+ * Called to indicate that data connection list changed.
+ *
+ * @param dataCallList List of the current active data connection.
+ */
+ public void onDataCallListChanged(List<DataCallResponse> dataCallList) {
+ IDataServiceCallback callback = mCallback.get();
+ if (callback != null) {
+ try {
+ callback.onDataCallListChanged(dataCallList);
+ } catch (RemoteException e) {
+ Rlog.e(mTag, "Failed to onDataCallListChanged on the remote");
+ }
+ }
+ }
+}
diff --git a/android/telephony/data/InterfaceAddress.java b/android/telephony/data/InterfaceAddress.java
deleted file mode 100644
index 00d212a..0000000
--- a/android/telephony/data/InterfaceAddress.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright 2017 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.telephony.data;
-
-import android.annotation.SystemApi;
-import android.net.NetworkUtils;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-
-/**
- * This class represents a Network Interface address. In short it's an IP address, a subnet mask
- * when the address is an IPv4 one. An IP address and a network prefix length in the case of IPv6
- * address.
- *
- * @hide
- */
-@SystemApi
-public final class InterfaceAddress implements Parcelable {
-
- private final InetAddress mInetAddress;
-
- private final int mPrefixLength;
-
- /**
- * @param inetAddress A {@link InetAddress} of the address
- * @param prefixLength The network prefix length for this address.
- */
- public InterfaceAddress(InetAddress inetAddress, int prefixLength) {
- mInetAddress = inetAddress;
- mPrefixLength = prefixLength;
- }
-
- /**
- * @param address The address in string format
- * @param prefixLength The network prefix length for this address.
- * @throws UnknownHostException
- */
- public InterfaceAddress(String address, int prefixLength) throws UnknownHostException {
- InetAddress ia;
- try {
- ia = NetworkUtils.numericToInetAddress(address);
- } catch (IllegalArgumentException e) {
- throw new UnknownHostException("Non-numeric ip addr=" + address);
- }
- mInetAddress = ia;
- mPrefixLength = prefixLength;
- }
-
- public InterfaceAddress(Parcel source) {
- mInetAddress = (InetAddress) source.readSerializable();
- mPrefixLength = source.readInt();
- }
-
- /**
- * @return an InetAddress for this address.
- */
- public InetAddress getAddress() { return mInetAddress; }
-
- /**
- * @return The network prefix length for this address.
- */
- public int getNetworkPrefixLength() { return mPrefixLength; }
-
- @Override
- public boolean equals (Object o) {
- if (this == o) return true;
-
- if (o == null || !(o instanceof InterfaceAddress)) {
- return false;
- }
-
- InterfaceAddress other = (InterfaceAddress) o;
- return this.mInetAddress.equals(other.mInetAddress)
- && this.mPrefixLength == other.mPrefixLength;
- }
-
- @Override
- public int hashCode() {
- return mInetAddress.hashCode() * 31 + mPrefixLength * 37;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public String toString() {
- return mInetAddress + "/" + mPrefixLength;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeSerializable(mInetAddress);
- dest.writeInt(mPrefixLength);
- }
-
- public static final Parcelable.Creator<InterfaceAddress> CREATOR =
- new Parcelable.Creator<InterfaceAddress>() {
- @Override
- public InterfaceAddress createFromParcel(Parcel source) {
- return new InterfaceAddress(source);
- }
-
- @Override
- public InterfaceAddress[] newArray(int size) {
- return new InterfaceAddress[size];
- }
- };
-}
diff --git a/android/telephony/euicc/EuiccCardManager.java b/android/telephony/euicc/EuiccCardManager.java
new file mode 100644
index 0000000..88bae33
--- /dev/null
+++ b/android/telephony/euicc/EuiccCardManager.java
@@ -0,0 +1,680 @@
+/*
+ * Copyright (C) 2018 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.telephony.euicc;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.service.euicc.EuiccProfileInfo;
+import android.util.Log;
+
+import com.android.internal.telephony.euicc.IAuthenticateServerCallback;
+import com.android.internal.telephony.euicc.ICancelSessionCallback;
+import com.android.internal.telephony.euicc.IDeleteProfileCallback;
+import com.android.internal.telephony.euicc.IDisableProfileCallback;
+import com.android.internal.telephony.euicc.IEuiccCardController;
+import com.android.internal.telephony.euicc.IGetAllProfilesCallback;
+import com.android.internal.telephony.euicc.IGetDefaultSmdpAddressCallback;
+import com.android.internal.telephony.euicc.IGetEuiccChallengeCallback;
+import com.android.internal.telephony.euicc.IGetEuiccInfo1Callback;
+import com.android.internal.telephony.euicc.IGetEuiccInfo2Callback;
+import com.android.internal.telephony.euicc.IGetProfileCallback;
+import com.android.internal.telephony.euicc.IGetRulesAuthTableCallback;
+import com.android.internal.telephony.euicc.IGetSmdsAddressCallback;
+import com.android.internal.telephony.euicc.IListNotificationsCallback;
+import com.android.internal.telephony.euicc.ILoadBoundProfilePackageCallback;
+import com.android.internal.telephony.euicc.IPrepareDownloadCallback;
+import com.android.internal.telephony.euicc.IRemoveNotificationFromListCallback;
+import com.android.internal.telephony.euicc.IResetMemoryCallback;
+import com.android.internal.telephony.euicc.IRetrieveNotificationCallback;
+import com.android.internal.telephony.euicc.IRetrieveNotificationListCallback;
+import com.android.internal.telephony.euicc.ISetDefaultSmdpAddressCallback;
+import com.android.internal.telephony.euicc.ISetNicknameCallback;
+import com.android.internal.telephony.euicc.ISwitchToProfileCallback;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * EuiccCardManager is the application interface to an eSIM card.
+ *
+ * @hide
+ *
+ * TODO(b/35851809): Make this a SystemApi.
+ */
+public class EuiccCardManager {
+ private static final String TAG = "EuiccCardManager";
+
+ /** Reason for canceling a profile download session */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "CANCEL_REASON_" }, value = {
+ CANCEL_REASON_END_USER_REJECTED,
+ CANCEL_REASON_POSTPONED,
+ CANCEL_REASON_TIMEOUT,
+ CANCEL_REASON_PPR_NOT_ALLOWED
+ })
+ public @interface CancelReason {}
+
+ /**
+ * The end user has rejected the download. The profile will be put into the error state and
+ * cannot be downloaded again without the operator's change.
+ */
+ public static final int CANCEL_REASON_END_USER_REJECTED = 0;
+
+ /** The download has been postponed and can be restarted later. */
+ public static final int CANCEL_REASON_POSTPONED = 1;
+
+ /** The download has been timed out and can be restarted later. */
+ public static final int CANCEL_REASON_TIMEOUT = 2;
+
+ /**
+ * The profile to be downloaded cannot be installed due to its policy rule is not allowed by
+ * the RAT (Rules Authorisation Table) on the eUICC or by other installed profiles. The
+ * download can be restarted later.
+ */
+ public static final int CANCEL_REASON_PPR_NOT_ALLOWED = 3;
+
+ /** Options for resetting eUICC memory */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "RESET_OPTION_" }, value = {
+ RESET_OPTION_DELETE_OPERATIONAL_PROFILES,
+ RESET_OPTION_DELETE_FIELD_LOADED_TEST_PROFILES,
+ RESET_OPTION_RESET_DEFAULT_SMDP_ADDRESS
+ })
+ public @interface ResetOption {}
+
+ /** Deletes all operational profiles. */
+ public static final int RESET_OPTION_DELETE_OPERATIONAL_PROFILES = 1;
+
+ /** Deletes all field-loaded testing profiles. */
+ public static final int RESET_OPTION_DELETE_FIELD_LOADED_TEST_PROFILES = 1 << 1;
+
+ /** Resets the default SM-DP+ address. */
+ public static final int RESET_OPTION_RESET_DEFAULT_SMDP_ADDRESS = 1 << 2;
+
+ /** Result code of execution with no error. */
+ public static final int RESULT_OK = 0;
+
+ /**
+ * Callback to receive the result of an eUICC card API.
+ *
+ * @param <T> Type of the result.
+ */
+ public interface ResultCallback<T> {
+ /**
+ * This method will be called when an eUICC card API call is completed.
+ *
+ * @param resultCode This can be {@link #RESULT_OK} or other positive values returned by the
+ * eUICC.
+ * @param result The result object. It can be null if the {@code resultCode} is not
+ * {@link #RESULT_OK}.
+ */
+ void onComplete(int resultCode, T result);
+ }
+
+ private final Context mContext;
+
+ /** @hide */
+ public EuiccCardManager(Context context) {
+ mContext = context;
+ }
+
+ private IEuiccCardController getIEuiccCardController() {
+ return IEuiccCardController.Stub.asInterface(
+ ServiceManager.getService("euicc_card_controller"));
+ }
+
+ /**
+ * Gets all the profiles on eUicc.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param callback The callback to get the result code and all the profiles.
+ */
+ public void getAllProfiles(String cardId, ResultCallback<EuiccProfileInfo[]> callback) {
+ try {
+ getIEuiccCardController().getAllProfiles(mContext.getOpPackageName(), cardId,
+ new IGetAllProfilesCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, EuiccProfileInfo[] profiles) {
+ callback.onComplete(resultCode, profiles);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling getAllProfiles", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the profile of the given iccid.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param iccid The iccid of the profile.
+ * @param callback The callback to get the result code and profile.
+ */
+ public void getProfile(String cardId, String iccid, ResultCallback<EuiccProfileInfo> callback) {
+ try {
+ getIEuiccCardController().getProfile(mContext.getOpPackageName(), cardId, iccid,
+ new IGetProfileCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, EuiccProfileInfo profile) {
+ callback.onComplete(resultCode, profile);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling getProfile", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Disables the profile of the given iccid.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param iccid The iccid of the profile.
+ * @param refresh Whether sending the REFRESH command to modem.
+ * @param callback The callback to get the result code.
+ */
+ public void disableProfile(String cardId, String iccid, boolean refresh,
+ ResultCallback<Void> callback) {
+ try {
+ getIEuiccCardController().disableProfile(mContext.getOpPackageName(), cardId, iccid,
+ refresh, new IDisableProfileCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode) {
+ callback.onComplete(resultCode, null);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling disableProfile", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Switches from the current profile to another profile. The current profile will be disabled
+ * and the specified profile will be enabled.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param iccid The iccid of the profile to switch to.
+ * @param refresh Whether sending the REFRESH command to modem.
+ * @param callback The callback to get the result code and the EuiccProfileInfo enabled.
+ */
+ public void switchToProfile(String cardId, String iccid, boolean refresh,
+ ResultCallback<EuiccProfileInfo> callback) {
+ try {
+ getIEuiccCardController().switchToProfile(mContext.getOpPackageName(), cardId, iccid,
+ refresh, new ISwitchToProfileCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, EuiccProfileInfo profile) {
+ callback.onComplete(resultCode, profile);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling switchToProfile", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets the nickname of the profile of the given iccid.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param iccid The iccid of the profile.
+ * @param nickname The nickname of the profile.
+ * @param callback The callback to get the result code.
+ */
+ public void setNickname(String cardId, String iccid, String nickname,
+ ResultCallback<Void> callback) {
+ try {
+ getIEuiccCardController().setNickname(mContext.getOpPackageName(), cardId, iccid,
+ nickname, new ISetNicknameCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode) {
+ callback.onComplete(resultCode, null);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling setNickname", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Deletes the profile of the given iccid from eUICC.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param iccid The iccid of the profile.
+ * @param callback The callback to get the result code.
+ */
+ public void deleteProfile(String cardId, String iccid, ResultCallback<Void> callback) {
+ try {
+ getIEuiccCardController().deleteProfile(mContext.getOpPackageName(), cardId, iccid,
+ new IDeleteProfileCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode) {
+ callback.onComplete(resultCode, null);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling deleteProfile", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Resets the eUICC memory.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param options Bits of the options of resetting which parts of the eUICC memory. See
+ * EuiccCard for details.
+ * @param callback The callback to get the result code.
+ */
+ public void resetMemory(String cardId, @ResetOption int options, ResultCallback<Void> callback) {
+ try {
+ getIEuiccCardController().resetMemory(mContext.getOpPackageName(), cardId, options,
+ new IResetMemoryCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode) {
+ callback.onComplete(resultCode, null);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling resetMemory", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the default SM-DP+ address from eUICC.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param callback The callback to get the result code and the default SM-DP+ address.
+ */
+ public void getDefaultSmdpAddress(String cardId, ResultCallback<String> callback) {
+ try {
+ getIEuiccCardController().getDefaultSmdpAddress(mContext.getOpPackageName(), cardId,
+ new IGetDefaultSmdpAddressCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, String address) {
+ callback.onComplete(resultCode, address);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling getDefaultSmdpAddress", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the SM-DS address from eUICC.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param callback The callback to get the result code and the SM-DS address.
+ */
+ public void getSmdsAddress(String cardId, ResultCallback<String> callback) {
+ try {
+ getIEuiccCardController().getSmdsAddress(mContext.getOpPackageName(), cardId,
+ new IGetSmdsAddressCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, String address) {
+ callback.onComplete(resultCode, address);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling getSmdsAddress", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets the default SM-DP+ address of eUICC.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param defaultSmdpAddress The default SM-DP+ address to set.
+ * @param callback The callback to get the result code.
+ */
+ public void setDefaultSmdpAddress(String cardId, String defaultSmdpAddress, ResultCallback<Void> callback) {
+ try {
+ getIEuiccCardController().setDefaultSmdpAddress(mContext.getOpPackageName(), cardId,
+ defaultSmdpAddress,
+ new ISetDefaultSmdpAddressCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode) {
+ callback.onComplete(resultCode, null);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling setDefaultSmdpAddress", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets Rules Authorisation Table.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param callback the callback to get the result code and the rule authorisation table.
+ */
+ public void getRulesAuthTable(String cardId, ResultCallback<EuiccRulesAuthTable> callback) {
+ try {
+ getIEuiccCardController().getRulesAuthTable(mContext.getOpPackageName(), cardId,
+ new IGetRulesAuthTableCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, EuiccRulesAuthTable rat) {
+ callback.onComplete(resultCode, rat);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling getRulesAuthTable", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the eUICC challenge for new profile downloading.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param callback the callback to get the result code and the challenge.
+ */
+ public void getEuiccChallenge(String cardId, ResultCallback<byte[]> callback) {
+ try {
+ getIEuiccCardController().getEuiccChallenge(mContext.getOpPackageName(), cardId,
+ new IGetEuiccChallengeCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, byte[] challenge) {
+ callback.onComplete(resultCode, challenge);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling getEuiccChallenge", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the eUICC info1 defined in GSMA RSP v2.0+ for new profile downloading.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param callback the callback to get the result code and the info1.
+ */
+ public void getEuiccInfo1(String cardId, ResultCallback<byte[]> callback) {
+ try {
+ getIEuiccCardController().getEuiccInfo1(mContext.getOpPackageName(), cardId,
+ new IGetEuiccInfo1Callback.Stub() {
+ @Override
+ public void onComplete(int resultCode, byte[] info) {
+ callback.onComplete(resultCode, info);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling getEuiccInfo1", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the eUICC info2 defined in GSMA RSP v2.0+ for new profile downloading.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param callback the callback to get the result code and the info2.
+ */
+ public void getEuiccInfo2(String cardId, ResultCallback<byte[]> callback) {
+ try {
+ getIEuiccCardController().getEuiccInfo2(mContext.getOpPackageName(), cardId,
+ new IGetEuiccInfo2Callback.Stub() {
+ @Override
+ public void onComplete(int resultCode, byte[] info) {
+ callback.onComplete(resultCode, info);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling getEuiccInfo2", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Authenticates the SM-DP+ server by the eUICC.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param matchingId the activation code token defined in GSMA RSP v2.0+ or empty when it is not
+ * required.
+ * @param serverSigned1 ASN.1 data in byte array signed and returned by the SM-DP+ server.
+ * @param serverSignature1 ASN.1 data in byte array indicating a SM-DP+ signature which is
+ * returned by SM-DP+ server.
+ * @param euiccCiPkIdToBeUsed ASN.1 data in byte array indicating CI Public Key Identifier to be
+ * used by the eUICC for signature which is returned by SM-DP+ server. This is defined in
+ * GSMA RSP v2.0+.
+ * @param serverCertificate ASN.1 data in byte array indicating SM-DP+ Certificate returned by
+ * SM-DP+ server.
+ * @param callback the callback to get the result code and a byte array which represents a
+ * {@code AuthenticateServerResponse} defined in GSMA RSP v2.0+.
+ */
+ public void authenticateServer(String cardId, String matchingId, byte[] serverSigned1,
+ byte[] serverSignature1, byte[] euiccCiPkIdToBeUsed, byte[] serverCertificate,
+ ResultCallback<byte[]> callback) {
+ try {
+ getIEuiccCardController().authenticateServer(
+ mContext.getOpPackageName(),
+ cardId,
+ matchingId,
+ serverSigned1,
+ serverSignature1,
+ euiccCiPkIdToBeUsed,
+ serverCertificate,
+ new IAuthenticateServerCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, byte[] response) {
+ callback.onComplete(resultCode, response);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling authenticateServer", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Prepares the profile download request sent to SM-DP+.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param hashCc the hash of confirmation code. It can be null if there is no confirmation code
+ * required.
+ * @param smdpSigned2 ASN.1 data in byte array indicating the data to be signed by the SM-DP+
+ * returned by SM-DP+ server.
+ * @param smdpSignature2 ASN.1 data in byte array indicating the SM-DP+ signature returned by
+ * SM-DP+ server.
+ * @param smdpCertificate ASN.1 data in byte array indicating the SM-DP+ Certificate returned
+ * by SM-DP+ server.
+ * @param callback the callback to get the result code and a byte array which represents a
+ * {@code PrepareDownloadResponse} defined in GSMA RSP v2.0+
+ */
+ public void prepareDownload(String cardId, @Nullable byte[] hashCc, byte[] smdpSigned2,
+ byte[] smdpSignature2, byte[] smdpCertificate, ResultCallback<byte[]> callback) {
+ try {
+ getIEuiccCardController().prepareDownload(
+ mContext.getOpPackageName(),
+ cardId,
+ hashCc,
+ smdpSigned2,
+ smdpSignature2,
+ smdpCertificate,
+ new IPrepareDownloadCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, byte[] response) {
+ callback.onComplete(resultCode, response);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling prepareDownload", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Loads a downloaded bound profile package onto the eUICC.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param boundProfilePackage the Bound Profile Package data returned by SM-DP+ server.
+ * @param callback the callback to get the result code and a byte array which represents a
+ * {@code LoadBoundProfilePackageResponse} defined in GSMA RSP v2.0+.
+ */
+ public void loadBoundProfilePackage(String cardId, byte[] boundProfilePackage,
+ ResultCallback<byte[]> callback) {
+ try {
+ getIEuiccCardController().loadBoundProfilePackage(
+ mContext.getOpPackageName(),
+ cardId,
+ boundProfilePackage,
+ new ILoadBoundProfilePackageCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, byte[] response) {
+ callback.onComplete(resultCode, response);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling loadBoundProfilePackage", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Cancels the current profile download session.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param transactionId the transaction ID returned by SM-DP+ server.
+ * @param reason the cancel reason.
+ * @param callback the callback to get the result code and an byte[] which represents a
+ * {@code CancelSessionResponse} defined in GSMA RSP v2.0+.
+ */
+ public void cancelSession(String cardId, byte[] transactionId, @CancelReason int reason,
+ ResultCallback<byte[]> callback) {
+ try {
+ getIEuiccCardController().cancelSession(
+ mContext.getOpPackageName(),
+ cardId,
+ transactionId,
+ reason,
+ new ICancelSessionCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, byte[] response) {
+ callback.onComplete(resultCode, response);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling cancelSession", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Lists all notifications of the given {@code notificationEvents}.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param events bits of the event types ({@link EuiccNotification.Event}) to list.
+ * @param callback the callback to get the result code and the list of notifications.
+ */
+ public void listNotifications(String cardId, @EuiccNotification.Event int events,
+ ResultCallback<EuiccNotification[]> callback) {
+ try {
+ getIEuiccCardController().listNotifications(mContext.getOpPackageName(), cardId, events,
+ new IListNotificationsCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, EuiccNotification[] notifications) {
+ callback.onComplete(resultCode, notifications);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling listNotifications", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieves contents of all notification of the given {@code events}.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param events bits of the event types ({@link EuiccNotification.Event}) to list.
+ * @param callback the callback to get the result code and the list of notifications.
+ */
+ public void retrieveNotificationList(String cardId, @EuiccNotification.Event int events,
+ ResultCallback<EuiccNotification[]> callback) {
+ try {
+ getIEuiccCardController().retrieveNotificationList(mContext.getOpPackageName(), cardId,
+ events, new IRetrieveNotificationListCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, EuiccNotification[] notifications) {
+ callback.onComplete(resultCode, notifications);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling retrieveNotificationList", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieves the content of a notification of the given {@code seqNumber}.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param seqNumber the sequence number of the notification.
+ * @param callback the callback to get the result code and the notification.
+ */
+ public void retrieveNotification(String cardId, int seqNumber,
+ ResultCallback<EuiccNotification> callback) {
+ try {
+ getIEuiccCardController().retrieveNotification(mContext.getOpPackageName(), cardId,
+ seqNumber, new IRetrieveNotificationCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, EuiccNotification notification) {
+ callback.onComplete(resultCode, notification);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling retrieveNotification", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Removes a notification from eUICC.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param seqNumber the sequence number of the notification.
+ * @param callback the callback to get the result code.
+ */
+ public void removeNotificationFromList(String cardId, int seqNumber,
+ ResultCallback<Void> callback) {
+ try {
+ getIEuiccCardController().removeNotificationFromList(
+ mContext.getOpPackageName(),
+ cardId,
+ seqNumber,
+ new IRemoveNotificationFromListCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode) {
+ callback.onComplete(resultCode, null);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling removeNotificationFromList", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/android/telephony/euicc/EuiccManager.java b/android/telephony/euicc/EuiccManager.java
index 176057d..7f913ce 100644
--- a/android/telephony/euicc/EuiccManager.java
+++ b/android/telephony/euicc/EuiccManager.java
@@ -19,6 +19,7 @@
import android.annotation.Nullable;
import android.annotation.SdkConstant;
import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
@@ -60,6 +61,30 @@
public static final String ACTION_MANAGE_EMBEDDED_SUBSCRIPTIONS =
"android.telephony.euicc.action.MANAGE_EMBEDDED_SUBSCRIPTIONS";
+
+ /**
+ * Broadcast Action: The eUICC OTA status is changed.
+ * <p class="note">
+ * Requires the {@link android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission.
+ *
+ * <p class="note">This is a protected intent that can only be sent
+ * by the system.
+ * TODO(b/35851809): Make this a SystemApi.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_OTA_STATUS_CHANGED =
+ "android.telephony.euicc.action.OTA_STATUS_CHANGED";
+
+ /**
+ * Broadcast Action: The action sent to carrier app so it knows the carrier setup is not
+ * completed.
+ *
+ * TODO(b/35851809): Make this a public API.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_NOTIFY_CARRIER_SETUP =
+ "android.telephony.euicc.action.NOTIFY_CARRIER_SETUP";
+
/**
* Intent action to provision an embedded subscription.
*
@@ -251,8 +276,8 @@
*
* @return the status of eUICC OTA. If {@link #isEnabled()} is false or the eUICC is not ready,
* {@link OtaStatus#EUICC_OTA_STATUS_UNAVAILABLE} will be returned.
+ * TODO(b/35851809): Make this a SystemApi.
*/
- @SystemApi
public int getOtaStatus() {
if (!isEnabled()) {
return EUICC_OTA_STATUS_UNAVAILABLE;
@@ -574,7 +599,11 @@
}
}
- private static IEuiccController getIEuiccController() {
+ /**
+ * @hide
+ */
+ @TestApi
+ protected IEuiccController getIEuiccController() {
return IEuiccController.Stub.asInterface(ServiceManager.getService("econtroller"));
}
}
diff --git a/android/telephony/euicc/EuiccNotification.java b/android/telephony/euicc/EuiccNotification.java
new file mode 100644
index 0000000..ef3c1ce
--- /dev/null
+++ b/android/telephony/euicc/EuiccNotification.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2018 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.telephony.euicc;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * This represents a signed notification which is defined in SGP.22. It can be either a profile
+ * installation result or a notification generated for profile operations (e.g., enabling,
+ * disabling, or deleting).
+ *
+ * @hide
+ *
+ * TODO(b/35851809): Make this a @SystemApi.
+ */
+public class EuiccNotification implements Parcelable {
+ /** Event */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "EVENT_" }, value = {
+ EVENT_INSTALL,
+ EVENT_ENABLE,
+ EVENT_DISABLE,
+ EVENT_DELETE
+ })
+ public @interface Event {}
+
+ /** A profile is downloaded and installed. */
+ public static final int EVENT_INSTALL = 1;
+
+ /** A profile is enabled. */
+ public static final int EVENT_ENABLE = 1 << 1;
+
+ /** A profile is disabled. */
+ public static final int EVENT_DISABLE = 1 << 2;
+
+ /** A profile is deleted. */
+ public static final int EVENT_DELETE = 1 << 3;
+
+ /** Value of the bits of all above events */
+ @Event
+ public static final int ALL_EVENTS =
+ EVENT_INSTALL | EVENT_ENABLE | EVENT_DISABLE | EVENT_DELETE;
+
+ private final int mSeq;
+ private final String mTargetAddr;
+ @Event private final int mEvent;
+ @Nullable private final byte[] mData;
+
+ /**
+ * Creates an instance.
+ *
+ * @param seq The sequence number of this notification.
+ * @param targetAddr The target server where to send this notification.
+ * @param event The event which causes this notification.
+ * @param data The data which needs to be sent to the target server. This can be null for
+ * building a list of notification metadata without data.
+ */
+ public EuiccNotification(int seq, String targetAddr, @Event int event, @Nullable byte[] data) {
+ mSeq = seq;
+ mTargetAddr = targetAddr;
+ mEvent = event;
+ mData = data;
+ }
+
+ /** @return The sequence number of this notification. */
+ public int getSeq() {
+ return mSeq;
+ }
+
+ /** @return The target server address where this notification should be sent to. */
+ public String getTargetAddr() {
+ return mTargetAddr;
+ }
+
+ /** @return The event of this notification. */
+ @Event
+ public int getEvent() {
+ return mEvent;
+ }
+
+ /** @return The notification data which needs to be sent to the target server. */
+ @Nullable
+ public byte[] getData() {
+ return mData;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ EuiccNotification that = (EuiccNotification) obj;
+ return mSeq == that.mSeq
+ && Objects.equals(mTargetAddr, that.mTargetAddr)
+ && mEvent == that.mEvent
+ && Arrays.equals(mData, that.mData);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + mSeq;
+ result = 31 * result + Objects.hashCode(mTargetAddr);
+ result = 31 * result + mEvent;
+ result = 31 * result + Arrays.hashCode(mData);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "EuiccNotification (seq="
+ + mSeq
+ + ", targetAddr="
+ + mTargetAddr
+ + ", event="
+ + mEvent
+ + ", data="
+ + (mData == null ? "null" : "byte[" + mData.length + "]")
+ + ")";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mSeq);
+ dest.writeString(mTargetAddr);
+ dest.writeInt(mEvent);
+ dest.writeByteArray(mData);
+ }
+
+ private EuiccNotification(Parcel source) {
+ mSeq = source.readInt();
+ mTargetAddr = source.readString();
+ mEvent = source.readInt();
+ mData = source.createByteArray();
+ }
+
+ public static final Creator<EuiccNotification> CREATOR =
+ new Creator<EuiccNotification>() {
+ @Override
+ public EuiccNotification createFromParcel(Parcel source) {
+ return new EuiccNotification(source);
+ }
+
+ @Override
+ public EuiccNotification[] newArray(int size) {
+ return new EuiccNotification[size];
+ }
+ };
+}
diff --git a/android/telephony/euicc/EuiccRulesAuthTable.java b/android/telephony/euicc/EuiccRulesAuthTable.java
new file mode 100644
index 0000000..7efe043
--- /dev/null
+++ b/android/telephony/euicc/EuiccRulesAuthTable.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2018 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.telephony.euicc;
+
+import android.annotation.IntDef;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.service.carrier.CarrierIdentifier;
+import android.service.euicc.EuiccProfileInfo;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+
+/**
+ * This represents the RAT (Rules Authorisation Table) stored on eUICC.
+ *
+ * @hide
+ *
+ * TODO(b/35851809): Make this a @SystemApi.
+ */
+public final class EuiccRulesAuthTable implements Parcelable {
+ /** Profile policy rule flags */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "POLICY_RULE_FLAG_" }, value = {
+ POLICY_RULE_FLAG_CONSENT_REQUIRED
+ })
+ public @interface PolicyRuleFlag {}
+
+ /** User consent is required to install the profile. */
+ public static final int POLICY_RULE_FLAG_CONSENT_REQUIRED = 1;
+
+ private final int[] mPolicyRules;
+ private final CarrierIdentifier[][] mCarrierIds;
+ private final int[] mPolicyRuleFlags;
+
+ /** This is used to build new {@link EuiccRulesAuthTable} instance. */
+ public static final class Builder {
+ private int[] mPolicyRules;
+ private CarrierIdentifier[][] mCarrierIds;
+ private int[] mPolicyRuleFlags;
+ private int mPosition;
+
+ /**
+ * Creates a new builder.
+ *
+ * @param ruleNum The number of authorisation rules in the table.
+ */
+ public Builder(int ruleNum) {
+ mPolicyRules = new int[ruleNum];
+ mCarrierIds = new CarrierIdentifier[ruleNum][];
+ mPolicyRuleFlags = new int[ruleNum];
+ }
+
+ /**
+ * Builds the RAT instance. This builder should not be used anymore after this method is
+ * called, otherwise {@link NullPointerException} will be thrown.
+ */
+ public EuiccRulesAuthTable build() {
+ if (mPosition != mPolicyRules.length) {
+ throw new IllegalStateException(
+ "Not enough rules are added, expected: "
+ + mPolicyRules.length
+ + ", added: "
+ + mPosition);
+ }
+ return new EuiccRulesAuthTable(mPolicyRules, mCarrierIds, mPolicyRuleFlags);
+ }
+
+ /**
+ * Adds an authorisation rule.
+ *
+ * @throws ArrayIndexOutOfBoundsException If the {@code mPosition} is larger than the size
+ * this table.
+ */
+ public Builder add(int policyRules, CarrierIdentifier[] carrierId, int policyRuleFlags) {
+ if (mPosition >= mPolicyRules.length) {
+ throw new ArrayIndexOutOfBoundsException(mPosition);
+ }
+ mPolicyRules[mPosition] = policyRules;
+ mCarrierIds[mPosition] = carrierId;
+ mPolicyRuleFlags[mPosition] = policyRuleFlags;
+ mPosition++;
+ return this;
+ }
+ }
+
+ /**
+ * @param mccRule A 2-character or 3-character string which can be either MCC or MNC. The
+ * character 'E' is used as a wild char to match any digit.
+ * @param mcc A 2-character or 3-character string which can be either MCC or MNC.
+ * @return Whether the {@code mccRule} matches {@code mcc}.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static boolean match(String mccRule, String mcc) {
+ if (mccRule.length() < mcc.length()) {
+ return false;
+ }
+ for (int i = 0; i < mccRule.length(); i++) {
+ // 'E' is the wild char to match any digit.
+ if (mccRule.charAt(i) == 'E'
+ || (i < mcc.length() && mccRule.charAt(i) == mcc.charAt(i))) {
+ continue;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private EuiccRulesAuthTable(int[] policyRules, CarrierIdentifier[][] carrierIds,
+ int[] policyRuleFlags) {
+ mPolicyRules = policyRules;
+ mCarrierIds = carrierIds;
+ mPolicyRuleFlags = policyRuleFlags;
+ }
+
+ /**
+ * Finds the index of the first authorisation rule matching the given policy and carrier id. If
+ * the returned index is not negative, the carrier is allowed to apply this policy to its
+ * profile.
+ *
+ * @param policy The policy rule.
+ * @param carrierId The carrier id.
+ * @return The index of authorization rule. If no rule is found, -1 will be returned.
+ */
+ public int findIndex(@EuiccProfileInfo.PolicyRule int policy, CarrierIdentifier carrierId) {
+ for (int i = 0; i < mPolicyRules.length; i++) {
+ if ((mPolicyRules[i] & policy) == 0) {
+ continue;
+ }
+ CarrierIdentifier[] carrierIds = mCarrierIds[i];
+ if (carrierIds == null || carrierIds.length == 0) {
+ continue;
+ }
+ for (int j = 0; j < carrierIds.length; j++) {
+ CarrierIdentifier ruleCarrierId = carrierIds[j];
+ if (!match(ruleCarrierId.getMcc(), carrierId.getMcc())
+ || !match(ruleCarrierId.getMnc(), carrierId.getMnc())) {
+ continue;
+ }
+ String gid = ruleCarrierId.getGid1();
+ if (!TextUtils.isEmpty(gid) && !gid.equals(carrierId.getGid1())) {
+ continue;
+ }
+ gid = ruleCarrierId.getGid2();
+ if (!TextUtils.isEmpty(gid) && !gid.equals(carrierId.getGid2())) {
+ continue;
+ }
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Tests if the entry in the table has the given policy rule flag.
+ *
+ * @param index The index of the entry.
+ * @param flag The policy rule flag to be tested.
+ * @throws ArrayIndexOutOfBoundsException If the {@code index} is negative or larger than the
+ * size of this table.
+ */
+ public boolean hasPolicyRuleFlag(int index, @PolicyRuleFlag int flag) {
+ if (index < 0 || index >= mPolicyRules.length) {
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return (mPolicyRuleFlags[index] & flag) != 0;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeIntArray(mPolicyRules);
+ for (CarrierIdentifier[] ids : mCarrierIds) {
+ dest.writeTypedArray(ids, flags);
+ }
+ dest.writeIntArray(mPolicyRuleFlags);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ EuiccRulesAuthTable that = (EuiccRulesAuthTable) obj;
+ if (mCarrierIds.length != that.mCarrierIds.length) {
+ return false;
+ }
+ for (int i = 0; i < mCarrierIds.length; i++) {
+ CarrierIdentifier[] carrierIds = mCarrierIds[i];
+ CarrierIdentifier[] thatCarrierIds = that.mCarrierIds[i];
+ if (carrierIds != null && thatCarrierIds != null) {
+ if (carrierIds.length != thatCarrierIds.length) {
+ return false;
+ }
+ for (int j = 0; j < carrierIds.length; j++) {
+ if (!carrierIds[j].equals(thatCarrierIds[j])) {
+ return false;
+ }
+ }
+ continue;
+ } else if (carrierIds == null && thatCarrierIds == null) {
+ continue;
+ }
+ return false;
+ }
+
+ return Arrays.equals(mPolicyRules, that.mPolicyRules)
+ && Arrays.equals(mPolicyRuleFlags, that.mPolicyRuleFlags);
+ }
+
+ private EuiccRulesAuthTable(Parcel source) {
+ mPolicyRules = source.createIntArray();
+ int len = mPolicyRules.length;
+ mCarrierIds = new CarrierIdentifier[len][];
+ for (int i = 0; i < len; i++) {
+ mCarrierIds[i] = source.createTypedArray(CarrierIdentifier.CREATOR);
+ }
+ mPolicyRuleFlags = source.createIntArray();
+ }
+
+ public static final Creator<EuiccRulesAuthTable> CREATOR =
+ new Creator<EuiccRulesAuthTable>() {
+ @Override
+ public EuiccRulesAuthTable createFromParcel(Parcel source) {
+ return new EuiccRulesAuthTable(source);
+ }
+
+ @Override
+ public EuiccRulesAuthTable[] newArray(int size) {
+ return new EuiccRulesAuthTable[size];
+ }
+ };
+}
diff --git a/android/telephony/ims/ImsService.java b/android/telephony/ims/ImsService.java
index 8230eaf..aaa0f08 100644
--- a/android/telephony/ims/ImsService.java
+++ b/android/telephony/ims/ImsService.java
@@ -26,12 +26,14 @@
import android.telephony.ims.feature.ImsFeature;
import android.telephony.ims.feature.MMTelFeature;
import android.telephony.ims.feature.RcsFeature;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
import android.util.Log;
import android.util.SparseArray;
import com.android.ims.internal.IImsFeatureStatusCallback;
import com.android.ims.internal.IImsMMTelFeature;
import com.android.ims.internal.IImsRcsFeature;
+import com.android.ims.internal.IImsRegistration;
import com.android.ims.internal.IImsServiceController;
import com.android.internal.annotations.VisibleForTesting;
@@ -113,6 +115,12 @@
throws RemoteException {
ImsService.this.removeImsFeature(slotId, featureType, c);
}
+
+ @Override
+ public IImsRegistration getRegistration(int slotId) throws RemoteException {
+ ImsRegistrationImplBase r = ImsService.this.getRegistration(slotId);
+ return r != null ? r.getBinder() : null;
+ }
};
/**
@@ -174,6 +182,8 @@
f.setSlotId(slotId);
f.addImsFeatureStatusCallback(c);
addImsFeature(slotId, featureType, f);
+ // TODO: Remove once new onFeatureReady AIDL is merged in.
+ f.onFeatureReady();
}
private void addImsFeature(int slotId, int featureType, ImsFeature f) {
@@ -236,4 +246,13 @@
public @Nullable RcsFeature onCreateRcsFeature(int slotId) {
return null;
}
+
+ /**
+ * @param slotId The slot that is associated with the IMS Registration.
+ * @return the ImsRegistration implementation associated with the slot.
+ * @hide
+ */
+ public ImsRegistrationImplBase getRegistration(int slotId) {
+ return new ImsRegistrationImplBase();
+ }
}
diff --git a/android/telephony/ims/feature/ImsFeature.java b/android/telephony/ims/feature/ImsFeature.java
index ca4a210..d47cea3 100644
--- a/android/telephony/ims/feature/ImsFeature.java
+++ b/android/telephony/ims/feature/ImsFeature.java
@@ -96,7 +96,7 @@
new WeakHashMap<IImsFeatureStatusCallback, Boolean>());
private @ImsState int mState = STATE_NOT_AVAILABLE;
private int mSlotId = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
- private Context mContext;
+ protected Context mContext;
public void setContext(Context context) {
mContext = context;
diff --git a/android/telephony/ims/feature/MMTelFeature.java b/android/telephony/ims/feature/MMTelFeature.java
index 4e095e3..5197107 100644
--- a/android/telephony/ims/feature/MMTelFeature.java
+++ b/android/telephony/ims/feature/MMTelFeature.java
@@ -19,6 +19,7 @@
import android.app.PendingIntent;
import android.os.Message;
import android.os.RemoteException;
+import android.telephony.ims.internal.stub.SmsImplBase;
import com.android.ims.ImsCallProfile;
import com.android.ims.internal.IImsCallSession;
@@ -28,6 +29,7 @@
import com.android.ims.internal.IImsMMTelFeature;
import com.android.ims.internal.IImsMultiEndpoint;
import com.android.ims.internal.IImsRegistrationListener;
+import com.android.ims.internal.IImsSmsListener;
import com.android.ims.internal.IImsUt;
import com.android.ims.internal.ImsCallSession;
@@ -171,6 +173,42 @@
return MMTelFeature.this.getMultiEndpointInterface();
}
}
+
+ @Override
+ public void setSmsListener(IImsSmsListener l) throws RemoteException {
+ synchronized (mLock) {
+ MMTelFeature.this.setSmsListener(l);
+ }
+ }
+
+ @Override
+ public void sendSms(int token, int messageRef, String format, String smsc, boolean retry,
+ byte[] pdu) {
+ synchronized (mLock) {
+ MMTelFeature.this.sendSms(token, messageRef, format, smsc, retry, pdu);
+ }
+ }
+
+ @Override
+ public void acknowledgeSms(int token, int messageRef, int result) {
+ synchronized (mLock) {
+ MMTelFeature.this.acknowledgeSms(token, messageRef, result);
+ }
+ }
+
+ @Override
+ public void acknowledgeSmsReport(int token, int messageRef, int result) {
+ synchronized (mLock) {
+ MMTelFeature.this.acknowledgeSmsReport(token, messageRef, result);
+ }
+ }
+
+ @Override
+ public String getSmsFormat() {
+ synchronized (mLock) {
+ return MMTelFeature.this.getSmsFormat();
+ }
+ }
};
/**
@@ -346,6 +384,39 @@
return null;
}
+ public void setSmsListener(IImsSmsListener listener) {
+ getSmsImplementation().registerSmsListener(listener);
+ }
+
+ public void sendSms(int token, int messageRef, String format, String smsc, boolean isRetry,
+ byte[] pdu) {
+ getSmsImplementation().sendSms(token, messageRef, format, smsc, isRetry, pdu);
+ }
+
+ public void acknowledgeSms(int token, int messageRef,
+ @SmsImplBase.DeliverStatusResult int result) {
+ getSmsImplementation().acknowledgeSms(token, messageRef, result);
+ }
+
+ public void acknowledgeSmsReport(int token, int messageRef,
+ @SmsImplBase.StatusReportResult int result) {
+ getSmsImplementation().acknowledgeSmsReport(token, messageRef, result);
+ }
+
+ /**
+ * Must be overridden by IMS Provider to be able to support SMS over IMS. Otherwise a default
+ * non-functional implementation is returned.
+ *
+ * @return an instance of {@link SmsImplBase} which should be implemented by the IMS Provider.
+ */
+ protected SmsImplBase getSmsImplementation() {
+ return new SmsImplBase();
+ }
+
+ public String getSmsFormat() {
+ return getSmsImplementation().getSmsFormat();
+ }
+
@Override
public void onFeatureReady() {
diff --git a/android/telephony/ims/internal/ImsService.java b/android/telephony/ims/internal/ImsService.java
index b7c8ca0..afaf332 100644
--- a/android/telephony/ims/internal/ImsService.java
+++ b/android/telephony/ims/internal/ImsService.java
@@ -24,7 +24,6 @@
import android.telephony.ims.internal.aidl.IImsConfig;
import android.telephony.ims.internal.aidl.IImsMmTelFeature;
import android.telephony.ims.internal.aidl.IImsRcsFeature;
-import android.telephony.ims.internal.aidl.IImsRegistration;
import android.telephony.ims.internal.aidl.IImsServiceController;
import android.telephony.ims.internal.aidl.IImsServiceControllerListener;
import android.telephony.ims.internal.feature.ImsFeature;
@@ -32,11 +31,12 @@
import android.telephony.ims.internal.feature.RcsFeature;
import android.telephony.ims.internal.stub.ImsConfigImplBase;
import android.telephony.ims.internal.stub.ImsFeatureConfiguration;
-import android.telephony.ims.internal.stub.ImsRegistrationImplBase;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
import android.util.Log;
import android.util.SparseArray;
import com.android.ims.internal.IImsFeatureStatusCallback;
+import com.android.ims.internal.IImsRegistration;
import com.android.internal.annotations.VisibleForTesting;
/**
diff --git a/android/telephony/ims/internal/SmsImplBase.java b/android/telephony/ims/internal/SmsImplBase.java
deleted file mode 100644
index 47414cf..0000000
--- a/android/telephony/ims/internal/SmsImplBase.java
+++ /dev/null
@@ -1,260 +0,0 @@
-/*
- * Copyright (C) 2017 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.telephony.ims.internal;
-
-import android.annotation.IntDef;
-import android.annotation.SystemApi;
-import android.os.RemoteException;
-import android.telephony.SmsManager;
-import android.telephony.SmsMessage;
-import android.telephony.ims.internal.aidl.IImsSmsListener;
-import android.telephony.ims.internal.feature.MmTelFeature;
-import android.util.Log;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Base implementation for SMS over IMS.
- *
- * Any service wishing to provide SMS over IMS should extend this class and implement all methods
- * that the service supports.
- * @hide
- */
-public class SmsImplBase {
- private static final String LOG_TAG = "SmsImplBase";
-
- @IntDef({
- SEND_STATUS_OK,
- SEND_STATUS_ERROR,
- SEND_STATUS_ERROR_RETRY,
- SEND_STATUS_ERROR_FALLBACK
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface SendStatusResult {}
- /**
- * Message was sent successfully.
- */
- public static final int SEND_STATUS_OK = 1;
-
- /**
- * IMS provider failed to send the message and platform should not retry falling back to sending
- * the message using the radio.
- */
- public static final int SEND_STATUS_ERROR = 2;
-
- /**
- * IMS provider failed to send the message and platform should retry again after setting TP-RD bit
- * to high.
- */
- public static final int SEND_STATUS_ERROR_RETRY = 3;
-
- /**
- * IMS provider failed to send the message and platform should retry falling back to sending
- * the message using the radio.
- */
- public static final int SEND_STATUS_ERROR_FALLBACK = 4;
-
- @IntDef({
- DELIVER_STATUS_OK,
- DELIVER_STATUS_ERROR
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface DeliverStatusResult {}
- /**
- * Message was delivered successfully.
- */
- public static final int DELIVER_STATUS_OK = 1;
-
- /**
- * Message was not delivered.
- */
- public static final int DELIVER_STATUS_ERROR = 2;
-
- @IntDef({
- STATUS_REPORT_STATUS_OK,
- STATUS_REPORT_STATUS_ERROR
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface StatusReportResult {}
-
- /**
- * Status Report was set successfully.
- */
- public static final int STATUS_REPORT_STATUS_OK = 1;
-
- /**
- * Error while setting status report.
- */
- public static final int STATUS_REPORT_STATUS_ERROR = 2;
-
-
- // Lock for feature synchronization
- private final Object mLock = new Object();
- private IImsSmsListener mListener;
-
- /**
- * Registers a listener responsible for handling tasks like delivering messages.
- *
- * @param listener listener to register.
- *
- * @hide
- */
- public final void registerSmsListener(IImsSmsListener listener) {
- synchronized (mLock) {
- mListener = listener;
- }
- }
-
- /**
- * This method will be triggered by the platform when the user attempts to send an SMS. This
- * method should be implemented by the IMS providers to provide implementation of sending an SMS
- * over IMS.
- *
- * @param smsc the Short Message Service Center address.
- * @param format the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and
- * {@link SmsMessage#FORMAT_3GPP2}.
- * @param messageRef the message reference.
- * @param isRetry whether it is a retry of an already attempted message or not.
- * @param pdu PDUs representing the contents of the message.
- */
- public void sendSms(int messageRef, String format, String smsc, boolean isRetry, byte[] pdu) {
- // Base implementation returns error. Should be overridden.
- try {
- onSendSmsResult(messageRef, SEND_STATUS_ERROR, SmsManager.RESULT_ERROR_GENERIC_FAILURE);
- } catch (RemoteException e) {
- Log.e(LOG_TAG, "Can not send sms: " + e.getMessage());
- }
- }
-
- /**
- * This method will be triggered by the platform after {@link #onSmsReceived(String, byte[])} has
- * been called to deliver the result to the IMS provider.
- *
- * @param result result of delivering the message. Valid values are defined in
- * {@link DeliverStatusResult}
- * @param messageRef the message reference or -1 of unavailable.
- */
- public void acknowledgeSms(int messageRef, @DeliverStatusResult int result) {
-
- }
-
- /**
- * This method will be triggered by the platform after
- * {@link #onSmsStatusReportReceived(int, int, byte[])} has been called to provide the result to
- * the IMS provider.
- *
- * @param result result of delivering the message. Valid values are defined in
- * {@link StatusReportResult}
- * @param messageRef the message reference or -1 of unavailable.
- */
- public void acknowledgeSmsReport(int messageRef, @StatusReportResult int result) {
-
- }
-
- /**
- * This method should be triggered by the IMS providers when there is an incoming message. The
- * platform will deliver the message to the messages database and notify the IMS provider of the
- * result by calling {@link #acknowledgeSms(int, int)}.
- *
- * This method must not be called before {@link MmTelFeature#onFeatureReady()} is called.
- *
- * @param format the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and
- * {@link SmsMessage#FORMAT_3GPP2}.
- * @param pdu PDUs representing the contents of the message.
- * @throws IllegalStateException if called before {@link MmTelFeature#onFeatureReady()}
- */
- public final void onSmsReceived(String format, byte[] pdu) throws IllegalStateException {
- synchronized (mLock) {
- if (mListener == null) {
- throw new IllegalStateException("Feature not ready.");
- }
- try {
- mListener.onSmsReceived(format, pdu);
- acknowledgeSms(-1, DELIVER_STATUS_OK);
- } catch (RemoteException e) {
- Log.e(LOG_TAG, "Can not deliver sms: " + e.getMessage());
- acknowledgeSms(-1, DELIVER_STATUS_ERROR);
- }
- }
- }
-
- /**
- * This method should be triggered by the IMS providers to pass the result of the sent message
- * to the platform.
- *
- * This method must not be called before {@link MmTelFeature#onFeatureReady()} is called.
- *
- * @param messageRef the message reference. Should be between 0 and 255 per TS.123.040
- * @param status result of sending the SMS. Valid values are defined in {@link SendStatusResult}
- * @param reason reason in case status is failure. Valid values are:
- * {@link SmsManager#RESULT_ERROR_NONE},
- * {@link SmsManager#RESULT_ERROR_GENERIC_FAILURE},
- * {@link SmsManager#RESULT_ERROR_RADIO_OFF},
- * {@link SmsManager#RESULT_ERROR_NULL_PDU},
- * {@link SmsManager#RESULT_ERROR_NO_SERVICE},
- * {@link SmsManager#RESULT_ERROR_LIMIT_EXCEEDED},
- * {@link SmsManager#RESULT_ERROR_SHORT_CODE_NOT_ALLOWED},
- * {@link SmsManager#RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED}
- * @throws IllegalStateException if called before {@link MmTelFeature#onFeatureReady()}
- * @throws RemoteException if the connection to the framework is not available. If this happens
- * attempting to send the SMS should be aborted.
- */
- public final void onSendSmsResult(int messageRef, @SendStatusResult int status, int reason)
- throws IllegalStateException, RemoteException {
- synchronized (mLock) {
- if (mListener == null) {
- throw new IllegalStateException("Feature not ready.");
- }
- mListener.onSendSmsResult(messageRef, status, reason);
- }
- }
-
- /**
- * Sets the status report of the sent message.
- *
- * @param messageRef the message reference.
- * @param format the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and
- * {@link SmsMessage#FORMAT_3GPP2}.
- * @param pdu PDUs representing the content of the status report.
- * @throws IllegalStateException if called before {@link MmTelFeature#onFeatureReady()}
- */
- public final void onSmsStatusReportReceived(int messageRef, String format, byte[] pdu) {
- synchronized (mLock) {
- if (mListener == null) {
- throw new IllegalStateException("Feature not ready.");
- }
- try {
- mListener.onSmsStatusReportReceived(messageRef, format, pdu);
- } catch (RemoteException e) {
- Log.e(LOG_TAG, "Can not process sms status report: " + e.getMessage());
- acknowledgeSmsReport(messageRef, STATUS_REPORT_STATUS_ERROR);
- }
- }
- }
-
- /**
- * Returns the SMS format. Default is {@link SmsMessage#FORMAT_3GPP} unless overridden by IMS
- * Provider.
- *
- * @return the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and
- * {@link SmsMessage#FORMAT_3GPP2}.
- */
- public String getSmsFormat() {
- return SmsMessage.FORMAT_3GPP;
- }
-
-}
diff --git a/android/telephony/ims/internal/feature/CapabilityChangeRequest.java b/android/telephony/ims/internal/feature/CapabilityChangeRequest.java
index 4d18873..5dbf077 100644
--- a/android/telephony/ims/internal/feature/CapabilityChangeRequest.java
+++ b/android/telephony/ims/internal/feature/CapabilityChangeRequest.java
@@ -18,7 +18,7 @@
import android.os.Parcel;
import android.os.Parcelable;
-import android.telephony.ims.internal.stub.ImsRegistrationImplBase;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
import android.util.ArraySet;
import java.util.ArrayList;
diff --git a/android/telephony/ims/internal/feature/MmTelFeature.java b/android/telephony/ims/internal/feature/MmTelFeature.java
index 2f350c8..9b576c7 100644
--- a/android/telephony/ims/internal/feature/MmTelFeature.java
+++ b/android/telephony/ims/internal/feature/MmTelFeature.java
@@ -21,15 +21,11 @@
import android.os.RemoteException;
import android.telecom.TelecomManager;
import android.telephony.ims.internal.ImsCallSessionListener;
-import android.telephony.ims.internal.SmsImplBase;
-import android.telephony.ims.internal.SmsImplBase.DeliverStatusResult;
-import android.telephony.ims.internal.SmsImplBase.StatusReportResult;
import android.telephony.ims.internal.aidl.IImsCallSessionListener;
import android.telephony.ims.internal.aidl.IImsCapabilityCallback;
import android.telephony.ims.internal.aidl.IImsMmTelFeature;
import android.telephony.ims.internal.aidl.IImsMmTelListener;
-import android.telephony.ims.internal.stub.ImsRegistrationImplBase;
-import android.telephony.ims.internal.aidl.IImsSmsListener;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
import android.telephony.ims.stub.ImsEcbmImplBase;
import android.telephony.ims.stub.ImsMultiEndpointImplBase;
import android.telephony.ims.stub.ImsUtImplBase;
@@ -68,11 +64,6 @@
}
@Override
- public void setSmsListener(IImsSmsListener l) throws RemoteException {
- MmTelFeature.this.setSmsListener(l);
- }
-
- @Override
public int getFeatureState() throws RemoteException {
synchronized (mLock) {
return MmTelFeature.this.getFeatureState();
@@ -152,34 +143,6 @@
IImsCapabilityCallback c) {
queryCapabilityConfigurationInternal(capability, radioTech, c);
}
-
- @Override
- public void sendSms(int messageRef, String format, String smsc, boolean retry, byte[] pdu) {
- synchronized (mLock) {
- MmTelFeature.this.sendSms(messageRef, format, smsc, retry, pdu);
- }
- }
-
- @Override
- public void acknowledgeSms(int messageRef, int result) {
- synchronized (mLock) {
- MmTelFeature.this.acknowledgeSms(messageRef, result);
- }
- }
-
- @Override
- public void acknowledgeSmsReport(int messageRef, int result) {
- synchronized (mLock) {
- MmTelFeature.this.acknowledgeSmsReport(messageRef, result);
- }
- }
-
- @Override
- public String getSmsFormat() {
- synchronized (mLock) {
- return MmTelFeature.this.getSmsFormat();
- }
- }
};
/**
@@ -261,6 +224,15 @@
}
/**
+ * Updates the Listener when the voice message count for IMS has changed.
+ * @param count an integer representing the new message count.
+ */
+ @Override
+ public void onVoiceMessageCountUpdate(int count) {
+
+ }
+
+ /**
* Called when the IMS provider receives an incoming call.
* @param c The {@link ImsCallSession} associated with the new call.
*/
@@ -282,10 +254,6 @@
}
}
- private void setSmsListener(IImsSmsListener listener) {
- getSmsImplementation().registerSmsListener(listener);
- }
-
private void queryCapabilityConfigurationInternal(int capability, int radioTech,
IImsCapabilityCallback c) {
boolean enabled = queryCapabilityConfiguration(capability, radioTech);
@@ -447,32 +415,6 @@
// Base Implementation - Should be overridden
}
- private void sendSms(int messageRef, String format, String smsc, boolean isRetry, byte[] pdu) {
- getSmsImplementation().sendSms(messageRef, format, smsc, isRetry, pdu);
- }
-
- private void acknowledgeSms(int messageRef, @DeliverStatusResult int result) {
- getSmsImplementation().acknowledgeSms(messageRef, result);
- }
-
- private void acknowledgeSmsReport(int messageRef, @StatusReportResult int result) {
- getSmsImplementation().acknowledgeSmsReport(messageRef, result);
- }
-
- private String getSmsFormat() {
- return getSmsImplementation().getSmsFormat();
- }
-
- /**
- * Must be overridden by IMS Provider to be able to support SMS over IMS. Otherwise a default
- * non-functional implementation is returned.
- *
- * @return an instance of {@link SmsImplBase} which should be implemented by the IMS Provider.
- */
- protected SmsImplBase getSmsImplementation() {
- return new SmsImplBase();
- }
-
/**{@inheritDoc}*/
@Override
public void onFeatureRemoved() {
diff --git a/android/telephony/ims/internal/stub/SmsImplBase.java b/android/telephony/ims/internal/stub/SmsImplBase.java
new file mode 100644
index 0000000..113dad4
--- /dev/null
+++ b/android/telephony/ims/internal/stub/SmsImplBase.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2018 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.telephony.ims.internal.stub;
+
+import android.annotation.IntDef;
+import android.os.RemoteException;
+import android.telephony.SmsManager;
+import android.telephony.SmsMessage;
+import android.telephony.ims.internal.feature.MmTelFeature;
+import android.util.Log;
+
+import com.android.ims.internal.IImsSmsListener;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Base implementation for SMS over IMS.
+ *
+ * Any service wishing to provide SMS over IMS should extend this class and implement all methods
+ * that the service supports.
+ * @hide
+ */
+public class SmsImplBase {
+ private static final String LOG_TAG = "SmsImplBase";
+
+ @IntDef({
+ SEND_STATUS_OK,
+ SEND_STATUS_ERROR,
+ SEND_STATUS_ERROR_RETRY,
+ SEND_STATUS_ERROR_FALLBACK
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SendStatusResult {}
+ /**
+ * Message was sent successfully.
+ */
+ public static final int SEND_STATUS_OK = 1;
+
+ /**
+ * IMS provider failed to send the message and platform should not retry falling back to sending
+ * the message using the radio.
+ */
+ public static final int SEND_STATUS_ERROR = 2;
+
+ /**
+ * IMS provider failed to send the message and platform should retry again after setting TP-RD bit
+ * to high.
+ */
+ public static final int SEND_STATUS_ERROR_RETRY = 3;
+
+ /**
+ * IMS provider failed to send the message and platform should retry falling back to sending
+ * the message using the radio.
+ */
+ public static final int SEND_STATUS_ERROR_FALLBACK = 4;
+
+ @IntDef({
+ DELIVER_STATUS_OK,
+ DELIVER_STATUS_ERROR
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DeliverStatusResult {}
+ /**
+ * Message was delivered successfully.
+ */
+ public static final int DELIVER_STATUS_OK = 1;
+
+ /**
+ * Message was not delivered.
+ */
+ public static final int DELIVER_STATUS_ERROR = 2;
+
+ @IntDef({
+ STATUS_REPORT_STATUS_OK,
+ STATUS_REPORT_STATUS_ERROR
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StatusReportResult {}
+
+ /**
+ * Status Report was set successfully.
+ */
+ public static final int STATUS_REPORT_STATUS_OK = 1;
+
+ /**
+ * Error while setting status report.
+ */
+ public static final int STATUS_REPORT_STATUS_ERROR = 2;
+
+
+ // Lock for feature synchronization
+ private final Object mLock = new Object();
+ private IImsSmsListener mListener;
+
+ /**
+ * Registers a listener responsible for handling tasks like delivering messages.
+ *
+ * @param listener listener to register.
+ *
+ * @hide
+ */
+ public final void registerSmsListener(IImsSmsListener listener) {
+ synchronized (mLock) {
+ mListener = listener;
+ }
+ }
+
+ /**
+ * This method will be triggered by the platform when the user attempts to send an SMS. This
+ * method should be implemented by the IMS providers to provide implementation of sending an SMS
+ * over IMS.
+ *
+ * @param token unique token generated by the platform that should be used when triggering
+ * callbacks for this specific message.
+ * @param messageRef the message reference.
+ * @param format the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and
+ * {@link SmsMessage#FORMAT_3GPP2}.
+ * @param smsc the Short Message Service Center address.
+ * @param isRetry whether it is a retry of an already attempted message or not.
+ * @param pdu PDUs representing the contents of the message.
+ */
+ public void sendSms(int token, int messageRef, String format, String smsc, boolean isRetry,
+ byte[] pdu) {
+ // Base implementation returns error. Should be overridden.
+ try {
+ onSendSmsResult(token, messageRef, SEND_STATUS_ERROR,
+ SmsManager.RESULT_ERROR_GENERIC_FAILURE);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Can not send sms: " + e.getMessage());
+ }
+ }
+
+ /**
+ * This method will be triggered by the platform after {@link #onSmsReceived(int, String, byte[])}
+ * has been called to deliver the result to the IMS provider.
+ *
+ * @param token token provided in {@link #onSmsReceived(int, String, byte[])}
+ * @param result result of delivering the message. Valid values are defined in
+ * {@link DeliverStatusResult}
+ * @param messageRef the message reference
+ */
+ public void acknowledgeSms(int token, int messageRef, @DeliverStatusResult int result) {
+ Log.e(LOG_TAG, "acknowledgeSms() not implemented.");
+ }
+
+ /**
+ * This method will be triggered by the platform after
+ * {@link #onSmsStatusReportReceived(int, int, String, byte[])} has been called to provide the
+ * result to the IMS provider.
+ *
+ * @param token token provided in {@link #sendSms(int, int, String, String, boolean, byte[])}
+ * @param result result of delivering the message. Valid values are defined in
+ * {@link StatusReportResult}
+ * @param messageRef the message reference
+ */
+ public void acknowledgeSmsReport(int token, int messageRef, @StatusReportResult int result) {
+ Log.e(LOG_TAG, "acknowledgeSmsReport() not implemented.");
+ }
+
+ /**
+ * This method should be triggered by the IMS providers when there is an incoming message. The
+ * platform will deliver the message to the messages database and notify the IMS provider of the
+ * result by calling {@link #acknowledgeSms(int, int, int)}.
+ *
+ * This method must not be called before {@link MmTelFeature#onFeatureReady()} is called.
+ *
+ * @param token unique token generated by IMS providers that the platform will use to trigger
+ * callbacks for this message.
+ * @param format the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and
+ * {@link SmsMessage#FORMAT_3GPP2}.
+ * @param pdu PDUs representing the contents of the message.
+ * @throws IllegalStateException if called before {@link MmTelFeature#onFeatureReady()}
+ */
+ public final void onSmsReceived(int token, String format, byte[] pdu)
+ throws IllegalStateException {
+ synchronized (mLock) {
+ if (mListener == null) {
+ throw new IllegalStateException("Feature not ready.");
+ }
+ try {
+ mListener.onSmsReceived(token, format, pdu);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Can not deliver sms: " + e.getMessage());
+ acknowledgeSms(token, 0, DELIVER_STATUS_ERROR);
+ }
+ }
+ }
+
+ /**
+ * This method should be triggered by the IMS providers to pass the result of the sent message
+ * to the platform.
+ *
+ * This method must not be called before {@link MmTelFeature#onFeatureReady()} is called.
+ *
+ * @param token token provided in {@link #sendSms(int, int, String, String, boolean, byte[])}
+ * @param messageRef the message reference. Should be between 0 and 255 per TS.123.040
+ * @param status result of sending the SMS. Valid values are defined in {@link SendStatusResult}
+ * @param reason reason in case status is failure. Valid values are:
+ * {@link SmsManager#RESULT_ERROR_NONE},
+ * {@link SmsManager#RESULT_ERROR_GENERIC_FAILURE},
+ * {@link SmsManager#RESULT_ERROR_RADIO_OFF},
+ * {@link SmsManager#RESULT_ERROR_NULL_PDU},
+ * {@link SmsManager#RESULT_ERROR_NO_SERVICE},
+ * {@link SmsManager#RESULT_ERROR_LIMIT_EXCEEDED},
+ * {@link SmsManager#RESULT_ERROR_SHORT_CODE_NOT_ALLOWED},
+ * {@link SmsManager#RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED}
+ * @throws IllegalStateException if called before {@link MmTelFeature#onFeatureReady()}
+ * @throws RemoteException if the connection to the framework is not available. If this happens
+ * attempting to send the SMS should be aborted.
+ */
+ public final void onSendSmsResult(int token, int messageRef, @SendStatusResult int status,
+ int reason) throws IllegalStateException, RemoteException {
+ synchronized (mLock) {
+ if (mListener == null) {
+ throw new IllegalStateException("Feature not ready.");
+ }
+ mListener.onSendSmsResult(token, messageRef, status, reason);
+ }
+ }
+
+ /**
+ * Sets the status report of the sent message.
+ *
+ * @param token token provided in {@link #sendSms(int, int, String, String, boolean, byte[])}
+ * @param messageRef the message reference.
+ * @param format the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and
+ * {@link SmsMessage#FORMAT_3GPP2}.
+ * @param pdu PDUs representing the content of the status report.
+ * @throws IllegalStateException if called before {@link MmTelFeature#onFeatureReady()}
+ */
+ public final void onSmsStatusReportReceived(int token, int messageRef, String format,
+ byte[] pdu) {
+ synchronized (mLock) {
+ if (mListener == null) {
+ throw new IllegalStateException("Feature not ready.");
+ }
+ try {
+ mListener.onSmsStatusReportReceived(token, messageRef, format, pdu);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Can not process sms status report: " + e.getMessage());
+ acknowledgeSmsReport(token, messageRef, STATUS_REPORT_STATUS_ERROR);
+ }
+ }
+ }
+
+ /**
+ * Returns the SMS format. Default is {@link SmsMessage#FORMAT_3GPP} unless overridden by IMS
+ * Provider.
+ *
+ * @return the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and
+ * {@link SmsMessage#FORMAT_3GPP2}.
+ */
+ public String getSmsFormat() {
+ return SmsMessage.FORMAT_3GPP;
+ }
+}
diff --git a/android/telephony/ims/internal/stub/ImsRegistrationImplBase.java b/android/telephony/ims/stub/ImsRegistrationImplBase.java
similarity index 83%
rename from android/telephony/ims/internal/stub/ImsRegistrationImplBase.java
rename to android/telephony/ims/stub/ImsRegistrationImplBase.java
index 558b009..42af083 100644
--- a/android/telephony/ims/internal/stub/ImsRegistrationImplBase.java
+++ b/android/telephony/ims/stub/ImsRegistrationImplBase.java
@@ -14,16 +14,19 @@
* limitations under the License
*/
-package android.telephony.ims.internal.stub;
+package android.telephony.ims.stub;
import android.annotation.IntDef;
+import android.net.Uri;
+import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
-import android.telephony.ims.internal.aidl.IImsRegistration;
-import android.telephony.ims.internal.aidl.IImsRegistrationCallback;
import android.util.Log;
import com.android.ims.ImsReasonInfo;
+import com.android.ims.internal.IImsRegistration;
+import com.android.ims.internal.IImsRegistrationCallback;
+import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -62,23 +65,25 @@
// Registration states, used to notify new ImsRegistrationImplBase#Callbacks of the current
// state.
+ // The unknown state is set as the initialization state. This is so that we do not call back
+ // with NOT_REGISTERED in the case where the ImsService has not updated the registration state
+ // yet.
+ private static final int REGISTRATION_STATE_UNKNOWN = -1;
private static final int REGISTRATION_STATE_NOT_REGISTERED = 0;
private static final int REGISTRATION_STATE_REGISTERING = 1;
private static final int REGISTRATION_STATE_REGISTERED = 2;
-
/**
* Callback class for receiving Registration callback events.
+ * @hide
*/
- public static class Callback extends IImsRegistrationCallback.Stub {
-
+ public static class Callback {
/**
* Notifies the framework when the IMS Provider is connected to the IMS network.
*
* @param imsRadioTech the radio access technology. Valid values are defined in
* {@link ImsRegistrationTech}.
*/
- @Override
public void onRegistered(@ImsRegistrationTech int imsRadioTech) {
}
@@ -88,7 +93,6 @@
* @param imsRadioTech the radio access technology. Valid values are defined in
* {@link ImsRegistrationTech}.
*/
- @Override
public void onRegistering(@ImsRegistrationTech int imsRadioTech) {
}
@@ -97,7 +101,6 @@
*
* @param info the {@link ImsReasonInfo} associated with why registration was disconnected.
*/
- @Override
public void onDeregistered(ImsReasonInfo info) {
}
@@ -108,10 +111,19 @@
* @param imsRadioTech The {@link ImsRegistrationTech} type that has failed
* @param info A {@link ImsReasonInfo} that identifies the reason for failure.
*/
- @Override
public void onTechnologyChangeFailed(@ImsRegistrationTech int imsRadioTech,
ImsReasonInfo info) {
}
+
+ /**
+ * Returns a list of subscriber {@link Uri}s associated with this IMS subscription when
+ * it changes.
+ * @param uris new array of subscriber {@link Uri}s that are associated with this IMS
+ * subscription.
+ */
+ public void onSubscriberAssociatedUriChanged(Uri[] uris) {
+
+ }
}
private final IImsRegistration mBinder = new IImsRegistration.Stub() {
@@ -139,9 +151,9 @@
private @ImsRegistrationTech
int mConnectionType = REGISTRATION_TECH_NONE;
// Locked on mLock
- private int mRegistrationState = REGISTRATION_STATE_NOT_REGISTERED;
- // Locked on mLock
- private ImsReasonInfo mLastDisconnectCause;
+ private int mRegistrationState = REGISTRATION_STATE_UNKNOWN;
+ // Locked on mLock, create unspecified disconnect cause.
+ private ImsReasonInfo mLastDisconnectCause = new ImsReasonInfo();
public final IImsRegistration getBinder() {
return mBinder;
@@ -221,6 +233,17 @@
});
}
+ public final void onSubscriberAssociatedUriChanged(Uri[] uris) {
+ mCallbacks.broadcast((c) -> {
+ try {
+ c.onSubscriberAssociatedUriChanged(uris);
+ } catch (RemoteException e) {
+ Log.w(LOG_TAG, e + " " + "onSubscriberAssociatedUriChanged() - Skipping " +
+ "callback.");
+ }
+ });
+ }
+
private void updateToState(@ImsRegistrationTech int connType, int newState) {
synchronized (mLock) {
mConnectionType = connType;
@@ -241,7 +264,8 @@
}
}
- private @ImsRegistrationTech int getConnectionType() {
+ @VisibleForTesting
+ public final @ImsRegistrationTech int getConnectionType() {
synchronized (mLock) {
return mConnectionType;
}
@@ -271,6 +295,10 @@
c.onRegistered(getConnectionType());
break;
}
+ case REGISTRATION_STATE_UNKNOWN: {
+ // Do not callback if the state has not been updated yet by the ImsService.
+ break;
+ }
}
}
}
diff --git a/android/test/IsolatedContext.java b/android/test/IsolatedContext.java
index 0b77c00..6e4c41e 100644
--- a/android/test/IsolatedContext.java
+++ b/android/test/IsolatedContext.java
@@ -17,12 +17,6 @@
package android.test;
import android.accounts.AccountManager;
-import android.accounts.AccountManagerCallback;
-import android.accounts.AccountManagerFuture;
-import android.accounts.AuthenticatorException;
-import android.accounts.OnAccountsUpdateListener;
-import android.accounts.OperationCanceledException;
-import android.accounts.Account;
import android.content.ContextWrapper;
import android.content.ContentResolver;
import android.content.Intent;
@@ -32,12 +26,10 @@
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.Uri;
-import android.os.Handler;
+import android.test.mock.MockAccountManager;
import java.io.File;
-import java.io.IOException;
import java.util.ArrayList;
-import java.util.concurrent.TimeUnit;
import java.util.List;
@@ -52,7 +44,7 @@
public class IsolatedContext extends ContextWrapper {
private ContentResolver mResolver;
- private final MockAccountManager mMockAccountManager;
+ private final AccountManager mMockAccountManager;
private List<Intent> mBroadcastIntents = new ArrayList<>();
@@ -60,7 +52,7 @@
ContentResolver resolver, Context targetContext) {
super(targetContext);
mResolver = resolver;
- mMockAccountManager = new MockAccountManager();
+ mMockAccountManager = MockAccountManager.newMockAccountManager(IsolatedContext.this);
}
/** Returns the list of intents that were broadcast since the last call to this method. */
@@ -123,71 +115,6 @@
return null;
}
- private class MockAccountManager extends AccountManager {
- public MockAccountManager() {
- super(IsolatedContext.this, null /* IAccountManager */, null /* handler */);
- }
-
- public void addOnAccountsUpdatedListener(OnAccountsUpdateListener listener,
- Handler handler, boolean updateImmediately) {
- // do nothing
- }
-
- public Account[] getAccounts() {
- return new Account[]{};
- }
-
- public AccountManagerFuture<Account[]> getAccountsByTypeAndFeatures(
- final String type, final String[] features,
- AccountManagerCallback<Account[]> callback, Handler handler) {
- return new MockAccountManagerFuture<Account[]>(new Account[0]);
- }
-
- public String blockingGetAuthToken(Account account, String authTokenType,
- boolean notifyAuthFailure)
- throws OperationCanceledException, IOException, AuthenticatorException {
- return null;
- }
-
-
- /**
- * A very simple AccountManagerFuture class
- * that returns what ever was passed in
- */
- private class MockAccountManagerFuture<T>
- implements AccountManagerFuture<T> {
-
- T mResult;
-
- public MockAccountManagerFuture(T result) {
- mResult = result;
- }
-
- public boolean cancel(boolean mayInterruptIfRunning) {
- return false;
- }
-
- public boolean isCancelled() {
- return false;
- }
-
- public boolean isDone() {
- return true;
- }
-
- public T getResult()
- throws OperationCanceledException, IOException, AuthenticatorException {
- return mResult;
- }
-
- public T getResult(long timeout, TimeUnit unit)
- throws OperationCanceledException, IOException, AuthenticatorException {
- return getResult();
- }
- }
-
- }
-
@Override
public File getFilesDir() {
return new File("/dev/null");
diff --git a/android/test/ProviderTestCase2.java b/android/test/ProviderTestCase2.java
index 1fa633e..be18b53 100644
--- a/android/test/ProviderTestCase2.java
+++ b/android/test/ProviderTestCase2.java
@@ -21,6 +21,7 @@
import android.content.Context;
import android.content.pm.ProviderInfo;
import android.content.res.Resources;
+import android.test.mock.MockContentProvider;
import android.test.mock.MockContext;
import android.test.mock.MockContentResolver;
import android.database.DatabaseUtils;
@@ -152,7 +153,7 @@
T instance = providerClass.newInstance();
ProviderInfo providerInfo = new ProviderInfo();
providerInfo.authority = authority;
- instance.attachInfoForTesting(context, providerInfo);
+ MockContentProvider.attachInfoForTesting(instance, context, providerInfo);
return instance;
}
diff --git a/android/test/RenamingDelegatingContext.java b/android/test/RenamingDelegatingContext.java
index fd33321..10ccebc 100644
--- a/android/test/RenamingDelegatingContext.java
+++ b/android/test/RenamingDelegatingContext.java
@@ -21,6 +21,7 @@
import android.content.ContentProvider;
import android.database.DatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase;
+import android.test.mock.MockContentProvider;
import android.util.Log;
import java.io.File;
@@ -71,7 +72,7 @@
if (allowAccessToExistingFilesAndDbs) {
mContext.makeExistingFilesAndDbsAccessible();
}
- mProvider.attachInfoForTesting(mContext, null);
+ MockContentProvider.attachInfoForTesting(mProvider, mContext, null);
return mProvider;
}
diff --git a/android/test/ServiceTestCase.java b/android/test/ServiceTestCase.java
index c8ff0f9..cd54955 100644
--- a/android/test/ServiceTestCase.java
+++ b/android/test/ServiceTestCase.java
@@ -23,6 +23,7 @@
import android.os.IBinder;
import android.test.mock.MockApplication;
+import android.test.mock.MockService;
import java.util.Random;
/**
@@ -163,14 +164,8 @@
if (getApplication() == null) {
setApplication(new MockApplication());
}
- mService.attach(
- getContext(),
- null, // ActivityThread not actually used in Service
- mServiceClass.getName(),
- null, // token not needed when not talking with the activity manager
- getApplication(),
- null // mocked services don't talk with the activity manager
- );
+ MockService.attachForTesting(
+ mService, getContext(), mServiceClass.getName(), getApplication());
assertNotNull(mService);
diff --git a/android/test/mock/MockAccountManager.java b/android/test/mock/MockAccountManager.java
new file mode 100644
index 0000000..c9b4c7b
--- /dev/null
+++ b/android/test/mock/MockAccountManager.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2017 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.test.mock;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.accounts.OnAccountsUpdateListener;
+import android.accounts.OperationCanceledException;
+import android.content.Context;
+import android.os.Handler;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A mock {@link android.accounts.AccountManager} class.
+ *
+ * <p>Provided for use by {@code android.test.IsolatedContext}.
+ *
+ * @deprecated Use a mocking framework like <a href="https://github.com/mockito/mockito">Mockito</a>.
+ * New tests should be written using the
+ * <a href="{@docRoot}
+ * tools/testing-support-library/index.html">Android Testing Support Library</a>.
+ */
+@Deprecated
+public class MockAccountManager {
+
+ /**
+ * Create a new mock {@link AccountManager} instance.
+ *
+ * @param context the {@link Context} to which the returned object belongs.
+ * @return the new instance.
+ */
+ public static AccountManager newMockAccountManager(Context context) {
+ return new MockAccountManagerImpl(context);
+ }
+
+ private MockAccountManager() {
+ }
+
+ private static class MockAccountManagerImpl extends AccountManager {
+
+ MockAccountManagerImpl(Context context) {
+ super(context, null /* IAccountManager */, null /* handler */);
+ }
+
+ public void addOnAccountsUpdatedListener(OnAccountsUpdateListener listener,
+ Handler handler, boolean updateImmediately) {
+ // do nothing
+ }
+
+ public Account[] getAccounts() {
+ return new Account[] {};
+ }
+
+ public AccountManagerFuture<Account[]> getAccountsByTypeAndFeatures(
+ final String type, final String[] features,
+ AccountManagerCallback<Account[]> callback, Handler handler) {
+ return new MockAccountManagerFuture<Account[]>(new Account[0]);
+ }
+
+ public String blockingGetAuthToken(Account account, String authTokenType,
+ boolean notifyAuthFailure)
+ throws OperationCanceledException, IOException, AuthenticatorException {
+ return null;
+ }
+ }
+
+ /**
+ * A very simple AccountManagerFuture class
+ * that returns what ever was passed in
+ */
+ private static class MockAccountManagerFuture<T>
+ implements AccountManagerFuture<T> {
+
+ T mResult;
+
+ MockAccountManagerFuture(T result) {
+ mResult = result;
+ }
+
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return false;
+ }
+
+ public boolean isCancelled() {
+ return false;
+ }
+
+ public boolean isDone() {
+ return true;
+ }
+
+ public T getResult()
+ throws OperationCanceledException, IOException, AuthenticatorException {
+ return mResult;
+ }
+
+ public T getResult(long timeout, TimeUnit unit)
+ throws OperationCanceledException, IOException, AuthenticatorException {
+ return getResult();
+ }
+ }
+}
diff --git a/android/test/mock/MockContentProvider.java b/android/test/mock/MockContentProvider.java
index d5f3ce8..b917fbd 100644
--- a/android/test/mock/MockContentProvider.java
+++ b/android/test/mock/MockContentProvider.java
@@ -277,4 +277,21 @@
public final IContentProvider getIContentProvider() {
return mIContentProvider;
}
+
+ /**
+ * Like {@link #attachInfo(Context, android.content.pm.ProviderInfo)}, but for use
+ * when directly instantiating the provider for testing.
+ *
+ * <p>Provided for use by {@code android.test.ProviderTestCase2} and
+ * {@code android.test.RenamingDelegatingContext}.
+ *
+ * @deprecated Use a mocking framework like <a href="https://github.com/mockito/mockito">Mockito</a>.
+ * New tests should be written using the
+ * <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
+ */
+ @Deprecated
+ public static void attachInfoForTesting(
+ ContentProvider provider, Context context, ProviderInfo providerInfo) {
+ provider.attachInfoForTesting(context, providerInfo);
+ }
}
diff --git a/android/test/mock/MockPackageManager.java b/android/test/mock/MockPackageManager.java
index ce8019f..1ddc52c 100644
--- a/android/test/mock/MockPackageManager.java
+++ b/android/test/mock/MockPackageManager.java
@@ -108,6 +108,12 @@
throw new UnsupportedOperationException();
}
+ /** @hide */
+ @Override
+ public Intent getCarLaunchIntentForPackage(String packageName) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public int[] getPackageGids(String packageName) throws NameNotFoundException {
throw new UnsupportedOperationException();
@@ -1090,15 +1096,6 @@
* @hide
*/
@Override
- public void installPackage(Uri packageURI, PackageInstallObserver observer,
- int flags, String installerPackageName) {
- throw new UnsupportedOperationException();
- }
-
- /**
- * @hide
- */
- @Override
public void addCrossProfileIntentFilter(IntentFilter filter, int sourceUserId, int targetUserId,
int flags) {
throw new UnsupportedOperationException();
@@ -1183,4 +1180,33 @@
public ArtManager getArtManager() {
throw new UnsupportedOperationException();
}
+
+ /**
+ * @hide
+ */
+ @Override
+ public void setHarmfulAppWarning(String packageName, CharSequence warning) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public CharSequence getHarmfulAppWarning(String packageName) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasSigningCertificate(
+ String packageName, byte[] certificate, @PackageManager.CertificateInputType int type) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasSigningCertificate(
+ int uid, byte[] certificate, @PackageManager.CertificateInputType int type) {
+ throw new UnsupportedOperationException();
+ }
+
}
diff --git a/android/test/mock/MockService.java b/android/test/mock/MockService.java
new file mode 100644
index 0000000..dbba4f3
--- /dev/null
+++ b/android/test/mock/MockService.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 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.test.mock;
+
+import android.app.Application;
+import android.app.Service;
+import android.content.Context;
+
+/**
+ * A mock {@link android.app.Service} class.
+ *
+ * <p>Provided for use by {@code android.test.ServiceTestCase}.
+ *
+ * @deprecated Use a mocking framework like <a href="https://github.com/mockito/mockito">Mockito</a>.
+ * New tests should be written using the
+ * <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
+ */
+@Deprecated
+public class MockService {
+
+ public static <T extends Service> void attachForTesting(Service service, Context context,
+ String serviceClassName,
+ Application application) {
+ service.attach(
+ context,
+ null, // ActivityThread not actually used in Service
+ serviceClassName,
+ null, // token not needed when not talking with the activity manager
+ application,
+ null // mocked services don't talk with the activity manager
+ );
+ }
+
+ private MockService() {
+ }
+}
diff --git a/android/text/BoringLayout.java b/android/text/BoringLayout.java
index ce38ebb..6fa5312 100644
--- a/android/text/BoringLayout.java
+++ b/android/text/BoringLayout.java
@@ -347,7 +347,14 @@
TextLine line = TextLine.obtain();
line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,
Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
- fm.width = (int) Math.ceil(line.metrics(fm));
+ if (text instanceof MeasuredText) {
+ MeasuredText mt = (MeasuredText) text;
+ // Reaching here means there is only one paragraph.
+ MeasuredParagraph mp = mt.getMeasuredParagraph(0);
+ fm.width = (int) Math.ceil(mp.getWidth(0, mp.getTextLength()));
+ } else {
+ fm.width = (int) Math.ceil(line.metrics(fm));
+ }
TextLine.recycle(line);
return fm;
diff --git a/android/text/DynamicLayout.java b/android/text/DynamicLayout.java
index 6bca37a..18431ca 100644
--- a/android/text/DynamicLayout.java
+++ b/android/text/DynamicLayout.java
@@ -1096,6 +1096,11 @@
public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
if (o instanceof UpdateLayout) {
+ if (start > end) {
+ // Bug: 67926915 start cannot be determined, fallback to reflow from start
+ // instead of causing an exception
+ start = 0;
+ }
reflow(s, start, end - start, end - start);
reflow(s, nstart, nend - nstart, nend - nstart);
}
diff --git a/android/text/Layout.java b/android/text/Layout.java
index bf4b6ac..aa97b2a 100644
--- a/android/text/Layout.java
+++ b/android/text/Layout.java
@@ -1917,10 +1917,10 @@
private static float measurePara(TextPaint paint, CharSequence text, int start, int end,
TextDirectionHeuristic textDir) {
- MeasuredText mt = null;
+ MeasuredParagraph mt = null;
TextLine tl = TextLine.obtain();
try {
- mt = MeasuredText.buildForBidi(text, start, end, textDir, mt);
+ mt = MeasuredParagraph.buildForBidi(text, start, end, textDir, mt);
final char[] chars = mt.getChars();
final int len = chars.length;
final Directions directions = mt.getDirections(0, len);
diff --git a/android/text/MeasuredParagraph.java b/android/text/MeasuredParagraph.java
new file mode 100644
index 0000000..45fbf6f
--- /dev/null
+++ b/android/text/MeasuredParagraph.java
@@ -0,0 +1,721 @@
+/*
+ * Copyright (C) 2010 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.text;
+
+import android.annotation.FloatRange;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Paint;
+import android.text.AutoGrowArray.ByteArray;
+import android.text.AutoGrowArray.FloatArray;
+import android.text.AutoGrowArray.IntArray;
+import android.text.Layout.Directions;
+import android.text.style.MetricAffectingSpan;
+import android.text.style.ReplacementSpan;
+import android.util.Pools.SynchronizedPool;
+
+import dalvik.annotation.optimization.CriticalNative;
+
+import libcore.util.NativeAllocationRegistry;
+
+import java.util.Arrays;
+
+/**
+ * MeasuredParagraph provides text information for rendering purpose.
+ *
+ * The first motivation of this class is identify the text directions and retrieving individual
+ * character widths. However retrieving character widths is slower than identifying text directions.
+ * Thus, this class provides several builder methods for specific purposes.
+ *
+ * - buildForBidi:
+ * Compute only text directions.
+ * - buildForMeasurement:
+ * Compute text direction and all character widths.
+ * - buildForStaticLayout:
+ * This is bit special. StaticLayout also needs to know text direction and character widths for
+ * line breaking, but all things are done in native code. Similarly, text measurement is done
+ * in native code. So instead of storing result to Java array, this keeps the result in native
+ * code since there is no good reason to move the results to Java layer.
+ *
+ * In addition to the character widths, some additional information is computed for each purposes,
+ * e.g. whole text length for measurement or font metrics for static layout.
+ *
+ * MeasuredParagraph is NOT a thread safe object.
+ * @hide
+ */
+public class MeasuredParagraph {
+ private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
+
+ private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
+ MeasuredParagraph.class.getClassLoader(), nGetReleaseFunc(), 1024);
+
+ private MeasuredParagraph() {} // Use build static functions instead.
+
+ private static final SynchronizedPool<MeasuredParagraph> sPool = new SynchronizedPool<>(1);
+
+ private static @NonNull MeasuredParagraph obtain() { // Use build static functions instead.
+ final MeasuredParagraph mt = sPool.acquire();
+ return mt != null ? mt : new MeasuredParagraph();
+ }
+
+ /**
+ * Recycle the MeasuredParagraph.
+ *
+ * Do not call any methods after you call this method.
+ */
+ public void recycle() {
+ release();
+ sPool.release(this);
+ }
+
+ // The casted original text.
+ //
+ // This may be null if the passed text is not a Spanned.
+ private @Nullable Spanned mSpanned;
+
+ // The start offset of the target range in the original text (mSpanned);
+ private @IntRange(from = 0) int mTextStart;
+
+ // The length of the target range in the original text.
+ private @IntRange(from = 0) int mTextLength;
+
+ // The copied character buffer for measuring text.
+ //
+ // The length of this array is mTextLength.
+ private @Nullable char[] mCopiedBuffer;
+
+ // The whole paragraph direction.
+ private @Layout.Direction int mParaDir;
+
+ // True if the text is LTR direction and doesn't contain any bidi characters.
+ private boolean mLtrWithoutBidi;
+
+ // The bidi level for individual characters.
+ //
+ // This is empty if mLtrWithoutBidi is true.
+ private @NonNull ByteArray mLevels = new ByteArray();
+
+ // The whole width of the text.
+ // See getWholeWidth comments.
+ private @FloatRange(from = 0.0f) float mWholeWidth;
+
+ // Individual characters' widths.
+ // See getWidths comments.
+ private @Nullable FloatArray mWidths = new FloatArray();
+
+ // The span end positions.
+ // See getSpanEndCache comments.
+ private @Nullable IntArray mSpanEndCache = new IntArray(4);
+
+ // The font metrics.
+ // See getFontMetrics comments.
+ private @Nullable IntArray mFontMetrics = new IntArray(4 * 4);
+
+ // The native MeasuredParagraph.
+ // See getNativePtr comments.
+ // Do not modify these members directly. Use bindNativeObject/unbindNativeObject instead.
+ private /* Maybe Zero */ long mNativePtr = 0;
+ private @Nullable Runnable mNativeObjectCleaner;
+
+ // Associate the native object to this Java object.
+ private void bindNativeObject(/* Non Zero*/ long nativePtr) {
+ mNativePtr = nativePtr;
+ mNativeObjectCleaner = sRegistry.registerNativeAllocation(this, nativePtr);
+ }
+
+ // Decouple the native object from this Java object and release the native object.
+ private void unbindNativeObject() {
+ if (mNativePtr != 0) {
+ mNativeObjectCleaner.run();
+ mNativePtr = 0;
+ }
+ }
+
+ // Following two objects are for avoiding object allocation.
+ private @NonNull TextPaint mCachedPaint = new TextPaint();
+ private @Nullable Paint.FontMetricsInt mCachedFm;
+
+ /**
+ * Releases internal buffers.
+ */
+ public void release() {
+ reset();
+ mLevels.clearWithReleasingLargeArray();
+ mWidths.clearWithReleasingLargeArray();
+ mFontMetrics.clearWithReleasingLargeArray();
+ mSpanEndCache.clearWithReleasingLargeArray();
+ }
+
+ /**
+ * Resets the internal state for starting new text.
+ */
+ private void reset() {
+ mSpanned = null;
+ mCopiedBuffer = null;
+ mWholeWidth = 0;
+ mLevels.clear();
+ mWidths.clear();
+ mFontMetrics.clear();
+ mSpanEndCache.clear();
+ unbindNativeObject();
+ }
+
+ /**
+ * Returns the length of the paragraph.
+ *
+ * This is always available.
+ */
+ public int getTextLength() {
+ return mTextLength;
+ }
+
+ /**
+ * Returns the characters to be measured.
+ *
+ * This is always available.
+ */
+ public @NonNull char[] getChars() {
+ return mCopiedBuffer;
+ }
+
+ /**
+ * Returns the paragraph direction.
+ *
+ * This is always available.
+ */
+ public @Layout.Direction int getParagraphDir() {
+ return mParaDir;
+ }
+
+ /**
+ * Returns the directions.
+ *
+ * This is always available.
+ */
+ public Directions getDirections(@IntRange(from = 0) int start, // inclusive
+ @IntRange(from = 0) int end) { // exclusive
+ if (mLtrWithoutBidi) {
+ return Layout.DIRS_ALL_LEFT_TO_RIGHT;
+ }
+
+ final int length = end - start;
+ return AndroidBidi.directions(mParaDir, mLevels.getRawArray(), start, mCopiedBuffer, start,
+ length);
+ }
+
+ /**
+ * Returns the whole text width.
+ *
+ * This is available only if the MeasuredParagraph is computed with buildForMeasurement.
+ * Returns 0 in other cases.
+ */
+ public @FloatRange(from = 0.0f) float getWholeWidth() {
+ return mWholeWidth;
+ }
+
+ /**
+ * Returns the individual character's width.
+ *
+ * This is available only if the MeasuredParagraph is computed with buildForMeasurement.
+ * Returns empty array in other cases.
+ */
+ public @NonNull FloatArray getWidths() {
+ return mWidths;
+ }
+
+ /**
+ * Returns the MetricsAffectingSpan end indices.
+ *
+ * If the input text is not a spanned string, this has one value that is the length of the text.
+ *
+ * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
+ * Returns empty array in other cases.
+ */
+ public @NonNull IntArray getSpanEndCache() {
+ return mSpanEndCache;
+ }
+
+ /**
+ * Returns the int array which holds FontMetrics.
+ *
+ * This array holds the repeat of top, bottom, ascent, descent of font metrics value.
+ *
+ * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
+ * Returns empty array in other cases.
+ */
+ public @NonNull IntArray getFontMetrics() {
+ return mFontMetrics;
+ }
+
+ /**
+ * Returns the native ptr of the MeasuredParagraph.
+ *
+ * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
+ * Returns 0 in other cases.
+ */
+ public /* Maybe Zero */ long getNativePtr() {
+ return mNativePtr;
+ }
+
+ /**
+ * Returns the width of the given range.
+ *
+ * This is not available if the MeasuredParagraph is computed with buildForBidi.
+ * Returns 0 if the MeasuredParagraph is computed with buildForBidi.
+ *
+ * @param start the inclusive start offset of the target region in the text
+ * @param end the exclusive end offset of the target region in the text
+ */
+ public float getWidth(int start, int end) {
+ if (mNativePtr == 0) {
+ // We have result in Java.
+ final float[] widths = mWidths.getRawArray();
+ float r = 0.0f;
+ for (int i = start; i < end; ++i) {
+ r += widths[i];
+ }
+ return r;
+ } else {
+ // We have result in native.
+ return nGetWidth(mNativePtr, start, end);
+ }
+ }
+
+ /**
+ * Generates new MeasuredParagraph for Bidi computation.
+ *
+ * If recycle is null, this returns new instance. If recycle is not null, this fills computed
+ * result to recycle and returns recycle.
+ *
+ * @param text the character sequence to be measured
+ * @param start the inclusive start offset of the target region in the text
+ * @param end the exclusive end offset of the target region in the text
+ * @param textDir the text direction
+ * @param recycle pass existing MeasuredParagraph if you want to recycle it.
+ *
+ * @return measured text
+ */
+ public static @NonNull MeasuredParagraph buildForBidi(@NonNull CharSequence text,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @NonNull TextDirectionHeuristic textDir,
+ @Nullable MeasuredParagraph recycle) {
+ final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
+ mt.resetAndAnalyzeBidi(text, start, end, textDir);
+ return mt;
+ }
+
+ /**
+ * Generates new MeasuredParagraph for measuring texts.
+ *
+ * If recycle is null, this returns new instance. If recycle is not null, this fills computed
+ * result to recycle and returns recycle.
+ *
+ * @param paint the paint to be used for rendering the text.
+ * @param text the character sequence to be measured
+ * @param start the inclusive start offset of the target region in the text
+ * @param end the exclusive end offset of the target region in the text
+ * @param textDir the text direction
+ * @param recycle pass existing MeasuredParagraph if you want to recycle it.
+ *
+ * @return measured text
+ */
+ public static @NonNull MeasuredParagraph buildForMeasurement(@NonNull TextPaint paint,
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @NonNull TextDirectionHeuristic textDir,
+ @Nullable MeasuredParagraph recycle) {
+ final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
+ mt.resetAndAnalyzeBidi(text, start, end, textDir);
+
+ mt.mWidths.resize(mt.mTextLength);
+ if (mt.mTextLength == 0) {
+ return mt;
+ }
+
+ if (mt.mSpanned == null) {
+ // No style change by MetricsAffectingSpan. Just measure all text.
+ mt.applyMetricsAffectingSpan(
+ paint, null /* spans */, start, end, 0 /* native static layout ptr */);
+ } else {
+ // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
+ int spanEnd;
+ for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
+ spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class);
+ MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
+ MetricAffectingSpan.class);
+ spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
+ mt.applyMetricsAffectingSpan(
+ paint, spans, spanStart, spanEnd, 0 /* native static layout ptr */);
+ }
+ }
+ return mt;
+ }
+
+ /**
+ * Generates new MeasuredParagraph for StaticLayout.
+ *
+ * If recycle is null, this returns new instance. If recycle is not null, this fills computed
+ * result to recycle and returns recycle.
+ *
+ * @param paint the paint to be used for rendering the text.
+ * @param text the character sequence to be measured
+ * @param start the inclusive start offset of the target region in the text
+ * @param end the exclusive end offset of the target region in the text
+ * @param textDir the text direction
+ * @param recycle pass existing MeasuredParagraph if you want to recycle it.
+ *
+ * @return measured text
+ */
+ public static @NonNull MeasuredParagraph buildForStaticLayout(
+ @NonNull TextPaint paint,
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @NonNull TextDirectionHeuristic textDir,
+ boolean computeHyphenation,
+ boolean computeLayout,
+ @Nullable MeasuredParagraph recycle) {
+ final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
+ mt.resetAndAnalyzeBidi(text, start, end, textDir);
+ if (mt.mTextLength == 0) {
+ // Need to build empty native measured text for StaticLayout.
+ // TODO: Stop creating empty measured text for empty lines.
+ long nativeBuilderPtr = nInitBuilder();
+ try {
+ mt.bindNativeObject(
+ nBuildNativeMeasuredParagraph(nativeBuilderPtr, mt.mCopiedBuffer,
+ computeHyphenation, computeLayout));
+ } finally {
+ nFreeBuilder(nativeBuilderPtr);
+ }
+ return mt;
+ }
+
+ long nativeBuilderPtr = nInitBuilder();
+ try {
+ if (mt.mSpanned == null) {
+ // No style change by MetricsAffectingSpan. Just measure all text.
+ mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end, nativeBuilderPtr);
+ mt.mSpanEndCache.append(end);
+ } else {
+ // There may be a MetricsAffectingSpan. Split into span transitions and apply
+ // styles.
+ int spanEnd;
+ for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
+ spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
+ MetricAffectingSpan.class);
+ MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
+ MetricAffectingSpan.class);
+ spans = TextUtils.removeEmptySpans(spans, mt.mSpanned,
+ MetricAffectingSpan.class);
+ mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd,
+ nativeBuilderPtr);
+ mt.mSpanEndCache.append(spanEnd);
+ }
+ }
+ mt.bindNativeObject(nBuildNativeMeasuredParagraph(nativeBuilderPtr, mt.mCopiedBuffer,
+ computeHyphenation, computeLayout));
+ } finally {
+ nFreeBuilder(nativeBuilderPtr);
+ }
+
+ return mt;
+ }
+
+ /**
+ * Reset internal state and analyzes text for bidirectional runs.
+ *
+ * @param text the character sequence to be measured
+ * @param start the inclusive start offset of the target region in the text
+ * @param end the exclusive end offset of the target region in the text
+ * @param textDir the text direction
+ */
+ private void resetAndAnalyzeBidi(@NonNull CharSequence text,
+ @IntRange(from = 0) int start, // inclusive
+ @IntRange(from = 0) int end, // exclusive
+ @NonNull TextDirectionHeuristic textDir) {
+ reset();
+ mSpanned = text instanceof Spanned ? (Spanned) text : null;
+ mTextStart = start;
+ mTextLength = end - start;
+
+ if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) {
+ mCopiedBuffer = new char[mTextLength];
+ }
+ TextUtils.getChars(text, start, end, mCopiedBuffer, 0);
+
+ // Replace characters associated with ReplacementSpan to U+FFFC.
+ if (mSpanned != null) {
+ ReplacementSpan[] spans = mSpanned.getSpans(start, end, ReplacementSpan.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int startInPara = mSpanned.getSpanStart(spans[i]) - start;
+ int endInPara = mSpanned.getSpanEnd(spans[i]) - start;
+ // The span interval may be larger and must be restricted to [start, end)
+ if (startInPara < 0) startInPara = 0;
+ if (endInPara > mTextLength) endInPara = mTextLength;
+ Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER);
+ }
+ }
+
+ if ((textDir == TextDirectionHeuristics.LTR
+ || textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR
+ || textDir == TextDirectionHeuristics.ANYRTL_LTR)
+ && TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) {
+ mLevels.clear();
+ mParaDir = Layout.DIR_LEFT_TO_RIGHT;
+ mLtrWithoutBidi = true;
+ } else {
+ final int bidiRequest;
+ if (textDir == TextDirectionHeuristics.LTR) {
+ bidiRequest = Layout.DIR_REQUEST_LTR;
+ } else if (textDir == TextDirectionHeuristics.RTL) {
+ bidiRequest = Layout.DIR_REQUEST_RTL;
+ } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
+ bidiRequest = Layout.DIR_REQUEST_DEFAULT_LTR;
+ } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
+ bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL;
+ } else {
+ final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
+ bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR;
+ }
+ mLevels.resize(mTextLength);
+ mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray());
+ mLtrWithoutBidi = false;
+ }
+ }
+
+ private void applyReplacementRun(@NonNull ReplacementSpan replacement,
+ @IntRange(from = 0) int start, // inclusive, in copied buffer
+ @IntRange(from = 0) int end, // exclusive, in copied buffer
+ /* Maybe Zero */ long nativeBuilderPtr) {
+ // Use original text. Shouldn't matter.
+ // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for
+ // backward compatibility? or Should we initialize them for getFontMetricsInt?
+ final float width = replacement.getSize(
+ mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
+ if (nativeBuilderPtr == 0) {
+ // Assigns all width to the first character. This is the same behavior as minikin.
+ mWidths.set(start, width);
+ if (end > start + 1) {
+ Arrays.fill(mWidths.getRawArray(), start + 1, end, 0.0f);
+ }
+ mWholeWidth += width;
+ } else {
+ nAddReplacementRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
+ width);
+ }
+ }
+
+ private void applyStyleRun(@IntRange(from = 0) int start, // inclusive, in copied buffer
+ @IntRange(from = 0) int end, // exclusive, in copied buffer
+ /* Maybe Zero */ long nativeBuilderPtr) {
+ if (nativeBuilderPtr != 0) {
+ mCachedPaint.getFontMetricsInt(mCachedFm);
+ }
+
+ if (mLtrWithoutBidi) {
+ // If the whole text is LTR direction, just apply whole region.
+ if (nativeBuilderPtr == 0) {
+ mWholeWidth += mCachedPaint.getTextRunAdvances(
+ mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */,
+ mWidths.getRawArray(), start);
+ } else {
+ nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
+ false /* isRtl */);
+ }
+ } else {
+ // If there is multiple bidi levels, split into individual bidi level and apply style.
+ byte level = mLevels.get(start);
+ // Note that the empty text or empty range won't reach this method.
+ // Safe to search from start + 1.
+ for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) {
+ if (levelEnd == end || mLevels.get(levelEnd) != level) { // transition point
+ final boolean isRtl = (level & 0x1) != 0;
+ if (nativeBuilderPtr == 0) {
+ final int levelLength = levelEnd - levelStart;
+ mWholeWidth += mCachedPaint.getTextRunAdvances(
+ mCopiedBuffer, levelStart, levelLength, levelStart, levelLength,
+ isRtl, mWidths.getRawArray(), levelStart);
+ } else {
+ nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), levelStart,
+ levelEnd, isRtl);
+ }
+ if (levelEnd == end) {
+ break;
+ }
+ levelStart = levelEnd;
+ level = mLevels.get(levelEnd);
+ }
+ }
+ }
+ }
+
+ private void applyMetricsAffectingSpan(
+ @NonNull TextPaint paint,
+ @Nullable MetricAffectingSpan[] spans,
+ @IntRange(from = 0) int start, // inclusive, in original text buffer
+ @IntRange(from = 0) int end, // exclusive, in original text buffer
+ /* Maybe Zero */ long nativeBuilderPtr) {
+ mCachedPaint.set(paint);
+ // XXX paint should not have a baseline shift, but...
+ mCachedPaint.baselineShift = 0;
+
+ final boolean needFontMetrics = nativeBuilderPtr != 0;
+
+ if (needFontMetrics && mCachedFm == null) {
+ mCachedFm = new Paint.FontMetricsInt();
+ }
+
+ ReplacementSpan replacement = null;
+ if (spans != null) {
+ for (int i = 0; i < spans.length; i++) {
+ MetricAffectingSpan span = spans[i];
+ if (span instanceof ReplacementSpan) {
+ // The last ReplacementSpan is effective for backward compatibility reasons.
+ replacement = (ReplacementSpan) span;
+ } else {
+ // TODO: No need to call updateMeasureState for ReplacementSpan as well?
+ span.updateMeasureState(mCachedPaint);
+ }
+ }
+ }
+
+ final int startInCopiedBuffer = start - mTextStart;
+ final int endInCopiedBuffer = end - mTextStart;
+
+ if (replacement != null) {
+ applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer,
+ nativeBuilderPtr);
+ } else {
+ applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeBuilderPtr);
+ }
+
+ if (needFontMetrics) {
+ if (mCachedPaint.baselineShift < 0) {
+ mCachedFm.ascent += mCachedPaint.baselineShift;
+ mCachedFm.top += mCachedPaint.baselineShift;
+ } else {
+ mCachedFm.descent += mCachedPaint.baselineShift;
+ mCachedFm.bottom += mCachedPaint.baselineShift;
+ }
+
+ mFontMetrics.append(mCachedFm.top);
+ mFontMetrics.append(mCachedFm.bottom);
+ mFontMetrics.append(mCachedFm.ascent);
+ mFontMetrics.append(mCachedFm.descent);
+ }
+ }
+
+ /**
+ * Returns the maximum index that the accumulated width not exceeds the width.
+ *
+ * If forward=false is passed, returns the minimum index from the end instead.
+ *
+ * This only works if the MeasuredParagraph is computed with buildForMeasurement.
+ * Undefined behavior in other case.
+ */
+ @IntRange(from = 0) int breakText(int limit, boolean forwards, float width) {
+ float[] w = mWidths.getRawArray();
+ if (forwards) {
+ int i = 0;
+ while (i < limit) {
+ width -= w[i];
+ if (width < 0.0f) break;
+ i++;
+ }
+ while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--;
+ return i;
+ } else {
+ int i = limit - 1;
+ while (i >= 0) {
+ width -= w[i];
+ if (width < 0.0f) break;
+ i--;
+ }
+ while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) {
+ i++;
+ }
+ return limit - i - 1;
+ }
+ }
+
+ /**
+ * Returns the length of the substring.
+ *
+ * This only works if the MeasuredParagraph is computed with buildForMeasurement.
+ * Undefined behavior in other case.
+ */
+ @FloatRange(from = 0.0f) float measure(int start, int limit) {
+ float width = 0;
+ float[] w = mWidths.getRawArray();
+ for (int i = start; i < limit; ++i) {
+ width += w[i];
+ }
+ return width;
+ }
+
+ private static native /* Non Zero */ long nInitBuilder();
+
+ /**
+ * Apply style to make native measured text.
+ *
+ * @param nativeBuilderPtr The native MeasuredParagraph builder pointer.
+ * @param paintPtr The native paint pointer to be applied.
+ * @param start The start offset in the copied buffer.
+ * @param end The end offset in the copied buffer.
+ * @param isRtl True if the text is RTL.
+ */
+ private static native void nAddStyleRun(/* Non Zero */ long nativeBuilderPtr,
+ /* Non Zero */ long paintPtr,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ boolean isRtl);
+
+ /**
+ * Apply ReplacementRun to make native measured text.
+ *
+ * @param nativeBuilderPtr The native MeasuredParagraph builder pointer.
+ * @param paintPtr The native paint pointer to be applied.
+ * @param start The start offset in the copied buffer.
+ * @param end The end offset in the copied buffer.
+ * @param width The width of the replacement.
+ */
+ private static native void nAddReplacementRun(/* Non Zero */ long nativeBuilderPtr,
+ /* Non Zero */ long paintPtr,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @FloatRange(from = 0) float width);
+
+ private static native long nBuildNativeMeasuredParagraph(/* Non Zero */ long nativeBuilderPtr,
+ @NonNull char[] text,
+ boolean computeHyphenation,
+ boolean computeLayout);
+
+ private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);
+
+ @CriticalNative
+ private static native float nGetWidth(/* Non Zero */ long nativePtr,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end);
+
+ @CriticalNative
+ private static native /* Non Zero */ long nGetReleaseFunc();
+}
diff --git a/android/text/MeasuredText_Delegate.java b/android/text/MeasuredParagraph_Delegate.java
similarity index 79%
rename from android/text/MeasuredText_Delegate.java
rename to android/text/MeasuredParagraph_Delegate.java
index adcc774..e4dbee0 100644
--- a/android/text/MeasuredText_Delegate.java
+++ b/android/text/MeasuredParagraph_Delegate.java
@@ -33,32 +33,32 @@
import libcore.util.NativeAllocationRegistry_Delegate;
/**
- * Delegate that provides implementation for native methods in {@link android.text.MeasuredText}
+ * Delegate that provides implementation for native methods in {@link android.text.MeasuredParagraph}
* <p/>
* Through the layoutlib_create tool, selected methods of StaticLayout have been replaced
* by calls to methods of the same name in this delegate class.
*
*/
-public class MeasuredText_Delegate {
+public class MeasuredParagraph_Delegate {
// ---- Builder delegate manager ----
- private static final DelegateManager<MeasuredTextBuilder> sBuilderManager =
- new DelegateManager<>(MeasuredTextBuilder.class);
- private static final DelegateManager<MeasuredText_Delegate> sManager =
- new DelegateManager<>(MeasuredText_Delegate.class);
+ private static final DelegateManager<MeasuredParagraphBuilder> sBuilderManager =
+ new DelegateManager<>(MeasuredParagraphBuilder.class);
+ private static final DelegateManager<MeasuredParagraph_Delegate> sManager =
+ new DelegateManager<>(MeasuredParagraph_Delegate.class);
private static long sFinalizer = -1;
private long mNativeBuilderPtr;
@LayoutlibDelegate
/*package*/ static long nInitBuilder() {
- return sBuilderManager.addNewDelegate(new MeasuredTextBuilder());
+ return sBuilderManager.addNewDelegate(new MeasuredParagraphBuilder());
}
/**
* Apply style to make native measured text.
*
- * @param nativeBuilderPtr The native MeasuredText builder pointer.
+ * @param nativeBuilderPtr The native MeasuredParagraph builder pointer.
* @param paintPtr The native paint pointer to be applied.
* @param start The start offset in the copied buffer.
* @param end The end offset in the copied buffer.
@@ -67,7 +67,7 @@
@LayoutlibDelegate
/*package*/ static void nAddStyleRun(long nativeBuilderPtr, long paintPtr, int start,
int end, boolean isRtl) {
- MeasuredTextBuilder builder = sBuilderManager.getDelegate(nativeBuilderPtr);
+ MeasuredParagraphBuilder builder = sBuilderManager.getDelegate(nativeBuilderPtr);
if (builder == null) {
return;
}
@@ -77,7 +77,7 @@
/**
* Apply ReplacementRun to make native measured text.
*
- * @param nativeBuilderPtr The native MeasuredText builder pointer.
+ * @param nativeBuilderPtr The native MeasuredParagraph builder pointer.
* @param paintPtr The native paint pointer to be applied.
* @param start The start offset in the copied buffer.
* @param end The end offset in the copied buffer.
@@ -86,7 +86,7 @@
@LayoutlibDelegate
/*package*/ static void nAddReplacementRun(long nativeBuilderPtr, long paintPtr, int start,
int end, float width) {
- MeasuredTextBuilder builder = sBuilderManager.getDelegate(nativeBuilderPtr);
+ MeasuredParagraphBuilder builder = sBuilderManager.getDelegate(nativeBuilderPtr);
if (builder == null) {
return;
}
@@ -94,8 +94,9 @@
}
@LayoutlibDelegate
- /*package*/ static long nBuildNativeMeasuredText(long nativeBuilderPtr, @NonNull char[] text) {
- MeasuredText_Delegate delegate = new MeasuredText_Delegate();
+ /*package*/ static long nBuildNativeMeasuredParagraph(long nativeBuilderPtr,
+ @NonNull char[] text, boolean computeHyphenation) {
+ MeasuredParagraph_Delegate delegate = new MeasuredParagraph_Delegate();
delegate.mNativeBuilderPtr = nativeBuilderPtr;
return sManager.addNewDelegate(delegate);
}
@@ -107,7 +108,7 @@
@LayoutlibDelegate
/*package*/ static long nGetReleaseFunc() {
- synchronized (MeasuredText_Delegate.class) {
+ synchronized (MeasuredParagraph_Delegate.class) {
if (sFinalizer == -1) {
sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(
sManager::removeJavaReferenceFor);
@@ -126,11 +127,11 @@
}
public static void computeRuns(long measuredTextPtr, Builder staticLayoutBuilder) {
- MeasuredText_Delegate delegate = sManager.getDelegate(measuredTextPtr);
+ MeasuredParagraph_Delegate delegate = sManager.getDelegate(measuredTextPtr);
if (delegate == null) {
return;
}
- MeasuredTextBuilder builder = sBuilderManager.getDelegate(delegate.mNativeBuilderPtr);
+ MeasuredParagraphBuilder builder = sBuilderManager.getDelegate(delegate.mNativeBuilderPtr);
if (builder == null) {
return;
}
@@ -172,7 +173,7 @@
}
}
- private static class MeasuredTextBuilder {
+ private static class MeasuredParagraphBuilder {
private final ArrayList<Run> mRuns = new ArrayList<>();
}
}
diff --git a/android/text/MeasuredText.java b/android/text/MeasuredText.java
index 14d6f9e..ff23395 100644
--- a/android/text/MeasuredText.java
+++ b/android/text/MeasuredText.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2010 The Android Open Source Project
+ * Copyright (C) 2017 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.
@@ -16,661 +16,398 @@
package android.text;
-import android.annotation.FloatRange;
import android.annotation.IntRange;
import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.graphics.Paint;
-import android.text.AutoGrowArray.ByteArray;
-import android.text.AutoGrowArray.FloatArray;
-import android.text.AutoGrowArray.IntArray;
-import android.text.Layout.Directions;
-import android.text.style.MetricAffectingSpan;
-import android.text.style.ReplacementSpan;
-import android.util.Pools.SynchronizedPool;
+import android.util.IntArray;
-import dalvik.annotation.optimization.CriticalNative;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.Preconditions;
-import libcore.util.NativeAllocationRegistry;
-
-import java.util.Arrays;
+import java.util.ArrayList;
/**
- * MeasuredText provides text information for rendering purpose.
- *
- * The first motivation of this class is identify the text directions and retrieving individual
- * character widths. However retrieving character widths is slower than identifying text directions.
- * Thus, this class provides several builder methods for specific purposes.
- *
- * - buildForBidi:
- * Compute only text directions.
- * - buildForMeasurement:
- * Compute text direction and all character widths.
- * - buildForStaticLayout:
- * This is bit special. StaticLayout also needs to know text direction and character widths for
- * line breaking, but all things are done in native code. Similarly, text measurement is done
- * in native code. So instead of storing result to Java array, this keeps the result in native
- * code since there is no good reason to move the results to Java layer.
- *
- * In addition to the character widths, some additional information is computed for each purposes,
- * e.g. whole text length for measurement or font metrics for static layout.
- *
- * MeasuredText is NOT a thread safe object.
- * @hide
+ * A text which has already been measured.
*/
-public class MeasuredText {
- private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
+public class MeasuredText implements Spanned {
+ private static final char LINE_FEED = '\n';
- private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
- MeasuredText.class.getClassLoader(), nGetReleaseFunc(), 1024);
+ // The original text.
+ private final @NonNull CharSequence mText;
- private MeasuredText() {} // Use build static functions instead.
+ // The inclusive start offset of the measuring target.
+ private final @IntRange(from = 0) int mStart;
- private static final SynchronizedPool<MeasuredText> sPool = new SynchronizedPool<>(1);
+ // The exclusive end offset of the measuring target.
+ private final @IntRange(from = 0) int mEnd;
- private static @NonNull MeasuredText obtain() { // Use build static functions instead.
- final MeasuredText mt = sPool.acquire();
- return mt != null ? mt : new MeasuredText();
- }
+ // The TextPaint used for measurement.
+ private final @NonNull TextPaint mPaint;
+
+ // The requested text direction.
+ private final @NonNull TextDirectionHeuristic mTextDir;
+
+ // The measured paragraph texts.
+ private final @NonNull MeasuredParagraph[] mMeasuredParagraphs;
+
+ // The sorted paragraph end offsets.
+ private final @NonNull int[] mParagraphBreakPoints;
+
+ // The break strategy for this measured text.
+ private final @Layout.BreakStrategy int mBreakStrategy;
+
+ // The hyphenation frequency for this measured text.
+ private final @Layout.HyphenationFrequency int mHyphenationFrequency;
/**
- * Recycle the MeasuredText.
- *
- * Do not call any methods after you call this method.
+ * A Builder for MeasuredText
*/
- public void recycle() {
- release();
- sPool.release(this);
- }
+ public static final class Builder {
+ // Mandatory parameters.
+ private final @NonNull CharSequence mText;
+ private final @NonNull TextPaint mPaint;
- // The casted original text.
- //
- // This may be null if the passed text is not a Spanned.
- private @Nullable Spanned mSpanned;
+ // Members to be updated by setters.
+ private @IntRange(from = 0) int mStart;
+ private @IntRange(from = 0) int mEnd;
+ private TextDirectionHeuristic mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
+ private @Layout.BreakStrategy int mBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
+ private @Layout.HyphenationFrequency int mHyphenationFrequency =
+ Layout.HYPHENATION_FREQUENCY_NORMAL;
- // The start offset of the target range in the original text (mSpanned);
- private @IntRange(from = 0) int mTextStart;
- // The length of the target range in the original text.
- private @IntRange(from = 0) int mTextLength;
+ /**
+ * Builder constructor
+ *
+ * @param text The text to be measured.
+ * @param paint The paint to be used for drawing.
+ */
+ public Builder(@NonNull CharSequence text, @NonNull TextPaint paint) {
+ Preconditions.checkNotNull(text);
+ Preconditions.checkNotNull(paint);
- // The copied character buffer for measuring text.
- //
- // The length of this array is mTextLength.
- private @Nullable char[] mCopiedBuffer;
-
- // The whole paragraph direction.
- private @Layout.Direction int mParaDir;
-
- // True if the text is LTR direction and doesn't contain any bidi characters.
- private boolean mLtrWithoutBidi;
-
- // The bidi level for individual characters.
- //
- // This is empty if mLtrWithoutBidi is true.
- private @NonNull ByteArray mLevels = new ByteArray();
-
- // The whole width of the text.
- // See getWholeWidth comments.
- private @FloatRange(from = 0.0f) float mWholeWidth;
-
- // Individual characters' widths.
- // See getWidths comments.
- private @Nullable FloatArray mWidths = new FloatArray();
-
- // The span end positions.
- // See getSpanEndCache comments.
- private @Nullable IntArray mSpanEndCache = new IntArray(4);
-
- // The font metrics.
- // See getFontMetrics comments.
- private @Nullable IntArray mFontMetrics = new IntArray(4 * 4);
-
- // The native MeasuredText.
- // See getNativePtr comments.
- // Do not modify these members directly. Use bindNativeObject/unbindNativeObject instead.
- private /* Maybe Zero */ long mNativePtr = 0;
- private @Nullable Runnable mNativeObjectCleaner;
-
- // Associate the native object to this Java object.
- private void bindNativeObject(/* Non Zero*/ long nativePtr) {
- mNativePtr = nativePtr;
- mNativeObjectCleaner = sRegistry.registerNativeAllocation(this, nativePtr);
- }
-
- // Decouple the native object from this Java object and release the native object.
- private void unbindNativeObject() {
- if (mNativePtr != 0) {
- mNativeObjectCleaner.run();
- mNativePtr = 0;
- }
- }
-
- // Following two objects are for avoiding object allocation.
- private @NonNull TextPaint mCachedPaint = new TextPaint();
- private @Nullable Paint.FontMetricsInt mCachedFm;
-
- /**
- * Releases internal buffers.
- */
- public void release() {
- reset();
- mLevels.clearWithReleasingLargeArray();
- mWidths.clearWithReleasingLargeArray();
- mFontMetrics.clearWithReleasingLargeArray();
- mSpanEndCache.clearWithReleasingLargeArray();
- }
-
- /**
- * Resets the internal state for starting new text.
- */
- private void reset() {
- mSpanned = null;
- mCopiedBuffer = null;
- mWholeWidth = 0;
- mLevels.clear();
- mWidths.clear();
- mFontMetrics.clear();
- mSpanEndCache.clear();
- unbindNativeObject();
- }
-
- /**
- * Returns the characters to be measured.
- *
- * This is always available.
- */
- public @NonNull char[] getChars() {
- return mCopiedBuffer;
- }
-
- /**
- * Returns the paragraph direction.
- *
- * This is always available.
- */
- public @Layout.Direction int getParagraphDir() {
- return mParaDir;
- }
-
- /**
- * Returns the directions.
- *
- * This is always available.
- */
- public Directions getDirections(@IntRange(from = 0) int start, // inclusive
- @IntRange(from = 0) int end) { // exclusive
- if (mLtrWithoutBidi) {
- return Layout.DIRS_ALL_LEFT_TO_RIGHT;
+ mText = text;
+ mPaint = paint;
+ mStart = 0;
+ mEnd = text.length();
}
- final int length = end - start;
- return AndroidBidi.directions(mParaDir, mLevels.getRawArray(), start, mCopiedBuffer, start,
- length);
- }
+ /**
+ * Set the range of measuring target.
+ *
+ * @param start The measuring target start offset in the text.
+ * @param end The measuring target end offset in the text.
+ */
+ public @NonNull Builder setRange(@IntRange(from = 0) int start,
+ @IntRange(from = 0) int end) {
+ Preconditions.checkArgumentInRange(start, 0, mText.length(), "start");
+ Preconditions.checkArgumentInRange(end, 0, mText.length(), "end");
+ Preconditions.checkArgument(start <= end, "The range is reversed.");
- /**
- * Returns the whole text width.
- *
- * This is available only if the MeasureText is computed with computeForMeasurement.
- * Returns 0 in other cases.
- */
- public @FloatRange(from = 0.0f) float getWholeWidth() {
- return mWholeWidth;
- }
-
- /**
- * Returns the individual character's width.
- *
- * This is available only if the MeasureText is computed with computeForMeasurement.
- * Returns empty array in other cases.
- */
- public @NonNull FloatArray getWidths() {
- return mWidths;
- }
-
- /**
- * Returns the MetricsAffectingSpan end indices.
- *
- * If the input text is not a spanned string, this has one value that is the length of the text.
- *
- * This is available only if the MeasureText is computed with computeForStaticLayout.
- * Returns empty array in other cases.
- */
- public @NonNull IntArray getSpanEndCache() {
- return mSpanEndCache;
- }
-
- /**
- * Returns the int array which holds FontMetrics.
- *
- * This array holds the repeat of top, bottom, ascent, descent of font metrics value.
- *
- * This is available only if the MeasureText is computed with computeForStaticLayout.
- * Returns empty array in other cases.
- */
- public @NonNull IntArray getFontMetrics() {
- return mFontMetrics;
- }
-
- /**
- * Returns the native ptr of the MeasuredText.
- *
- * This is available only if the MeasureText is computed with computeForStaticLayout.
- * Returns 0 in other cases.
- */
- public /* Maybe Zero */ long getNativePtr() {
- return mNativePtr;
- }
-
- /**
- * Generates new MeasuredText for Bidi computation.
- *
- * If recycle is null, this returns new instance. If recycle is not null, this fills computed
- * result to recycle and returns recycle.
- *
- * @param text the character sequence to be measured
- * @param start the inclusive start offset of the target region in the text
- * @param end the exclusive end offset of the target region in the text
- * @param textDir the text direction
- * @param recycle pass existing MeasuredText if you want to recycle it.
- *
- * @return measured text
- */
- public static @NonNull MeasuredText buildForBidi(@NonNull CharSequence text,
- @IntRange(from = 0) int start,
- @IntRange(from = 0) int end,
- @NonNull TextDirectionHeuristic textDir,
- @Nullable MeasuredText recycle) {
- final MeasuredText mt = recycle == null ? obtain() : recycle;
- mt.resetAndAnalyzeBidi(text, start, end, textDir);
- return mt;
- }
-
- /**
- * Generates new MeasuredText for measuring texts.
- *
- * If recycle is null, this returns new instance. If recycle is not null, this fills computed
- * result to recycle and returns recycle.
- *
- * @param paint the paint to be used for rendering the text.
- * @param text the character sequence to be measured
- * @param start the inclusive start offset of the target region in the text
- * @param end the exclusive end offset of the target region in the text
- * @param textDir the text direction
- * @param recycle pass existing MeasuredText if you want to recycle it.
- *
- * @return measured text
- */
- public static @NonNull MeasuredText buildForMeasurement(@NonNull TextPaint paint,
- @NonNull CharSequence text,
- @IntRange(from = 0) int start,
- @IntRange(from = 0) int end,
- @NonNull TextDirectionHeuristic textDir,
- @Nullable MeasuredText recycle) {
- final MeasuredText mt = recycle == null ? obtain() : recycle;
- mt.resetAndAnalyzeBidi(text, start, end, textDir);
-
- mt.mWidths.resize(mt.mTextLength);
- if (mt.mTextLength == 0) {
- return mt;
+ mStart = start;
+ mEnd = end;
+ return this;
}
- if (mt.mSpanned == null) {
- // No style change by MetricsAffectingSpan. Just measure all text.
- mt.applyMetricsAffectingSpan(
- paint, null /* spans */, start, end, 0 /* native static layout ptr */);
- } else {
- // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
- int spanEnd;
- for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
- spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class);
- MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
- MetricAffectingSpan.class);
- spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
- mt.applyMetricsAffectingSpan(
- paint, spans, spanStart, spanEnd, 0 /* native static layout ptr */);
- }
- }
- return mt;
- }
-
- /**
- * Generates new MeasuredText for StaticLayout.
- *
- * If recycle is null, this returns new instance. If recycle is not null, this fills computed
- * result to recycle and returns recycle.
- *
- * @param paint the paint to be used for rendering the text.
- * @param text the character sequence to be measured
- * @param start the inclusive start offset of the target region in the text
- * @param end the exclusive end offset of the target region in the text
- * @param textDir the text direction
- * @param recycle pass existing MeasuredText if you want to recycle it.
- *
- * @return measured text
- */
- public static @NonNull MeasuredText buildForStaticLayout(
- @NonNull TextPaint paint,
- @NonNull CharSequence text,
- @IntRange(from = 0) int start,
- @IntRange(from = 0) int end,
- @NonNull TextDirectionHeuristic textDir,
- @Nullable MeasuredText recycle) {
- final MeasuredText mt = recycle == null ? obtain() : recycle;
- mt.resetAndAnalyzeBidi(text, start, end, textDir);
- if (mt.mTextLength == 0) {
- // Need to build empty native measured text for StaticLayout.
- // TODO: Stop creating empty measured text for empty lines.
- long nativeBuilderPtr = nInitBuilder();
- try {
- mt.bindNativeObject(nBuildNativeMeasuredText(nativeBuilderPtr, mt.mCopiedBuffer));
- } finally {
- nFreeBuilder(nativeBuilderPtr);
- }
- return mt;
+ /**
+ * Set the text direction heuristic
+ *
+ * The default value is {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.
+ *
+ * @param textDir The text direction heuristic for resolving bidi behavior.
+ * @return this builder, useful for chaining.
+ */
+ public @NonNull Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
+ Preconditions.checkNotNull(textDir);
+ mTextDir = textDir;
+ return this;
}
- long nativeBuilderPtr = nInitBuilder();
- try {
- if (mt.mSpanned == null) {
- // No style change by MetricsAffectingSpan. Just measure all text.
- mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end, nativeBuilderPtr);
- mt.mSpanEndCache.append(end);
- } else {
- // There may be a MetricsAffectingSpan. Split into span transitions and apply
- // styles.
- int spanEnd;
- for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
- spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
- MetricAffectingSpan.class);
- MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
- MetricAffectingSpan.class);
- spans = TextUtils.removeEmptySpans(spans, mt.mSpanned,
- MetricAffectingSpan.class);
- mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd,
- nativeBuilderPtr);
- mt.mSpanEndCache.append(spanEnd);
- }
- }
- mt.bindNativeObject(nBuildNativeMeasuredText(nativeBuilderPtr, mt.mCopiedBuffer));
- } finally {
- nFreeBuilder(nativeBuilderPtr);
+ /**
+ * Set the break strategy
+ *
+ * The default value is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}.
+ *
+ * @param breakStrategy The break strategy.
+ * @return this builder, useful for chaining.
+ */
+ public @NonNull Builder setBreakStrategy(@Layout.BreakStrategy int breakStrategy) {
+ mBreakStrategy = breakStrategy;
+ return this;
}
- return mt;
- }
-
- /**
- * Reset internal state and analyzes text for bidirectional runs.
- *
- * @param text the character sequence to be measured
- * @param start the inclusive start offset of the target region in the text
- * @param end the exclusive end offset of the target region in the text
- * @param textDir the text direction
- */
- private void resetAndAnalyzeBidi(@NonNull CharSequence text,
- @IntRange(from = 0) int start, // inclusive
- @IntRange(from = 0) int end, // exclusive
- @NonNull TextDirectionHeuristic textDir) {
- reset();
- mSpanned = text instanceof Spanned ? (Spanned) text : null;
- mTextStart = start;
- mTextLength = end - start;
-
- if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) {
- mCopiedBuffer = new char[mTextLength];
- }
- TextUtils.getChars(text, start, end, mCopiedBuffer, 0);
-
- // Replace characters associated with ReplacementSpan to U+FFFC.
- if (mSpanned != null) {
- ReplacementSpan[] spans = mSpanned.getSpans(start, end, ReplacementSpan.class);
-
- for (int i = 0; i < spans.length; i++) {
- int startInPara = mSpanned.getSpanStart(spans[i]) - start;
- int endInPara = mSpanned.getSpanEnd(spans[i]) - start;
- // The span interval may be larger and must be restricted to [start, end)
- if (startInPara < 0) startInPara = 0;
- if (endInPara > mTextLength) endInPara = mTextLength;
- Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER);
- }
+ /**
+ * Set the hyphenation frequency
+ *
+ * The default value is {@link Layout#HYPHENATION_FREQUENCY_NORMAL}.
+ *
+ * @param hyphenationFrequency The hyphenation frequency.
+ * @return this builder, useful for chaining.
+ */
+ public @NonNull Builder setHyphenationFrequency(
+ @Layout.HyphenationFrequency int hyphenationFrequency) {
+ mHyphenationFrequency = hyphenationFrequency;
+ return this;
}
- if ((textDir == TextDirectionHeuristics.LTR ||
- textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR ||
- textDir == TextDirectionHeuristics.ANYRTL_LTR) &&
- TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) {
- mLevels.clear();
- mParaDir = Layout.DIR_LEFT_TO_RIGHT;
- mLtrWithoutBidi = true;
- } else {
- final int bidiRequest;
- if (textDir == TextDirectionHeuristics.LTR) {
- bidiRequest = Layout.DIR_REQUEST_LTR;
- } else if (textDir == TextDirectionHeuristics.RTL) {
- bidiRequest = Layout.DIR_REQUEST_RTL;
- } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
- bidiRequest = Layout.DIR_REQUEST_DEFAULT_LTR;
- } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
- bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL;
- } else {
- final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
- bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR;
- }
- mLevels.resize(mTextLength);
- mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray());
- mLtrWithoutBidi = false;
- }
- }
-
- private void applyReplacementRun(@NonNull ReplacementSpan replacement,
- @IntRange(from = 0) int start, // inclusive, in copied buffer
- @IntRange(from = 0) int end, // exclusive, in copied buffer
- /* Maybe Zero */ long nativeBuilderPtr) {
- // Use original text. Shouldn't matter.
- // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for
- // backward compatibility? or Should we initialize them for getFontMetricsInt?
- final float width = replacement.getSize(
- mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
- if (nativeBuilderPtr == 0) {
- // Assigns all width to the first character. This is the same behavior as minikin.
- mWidths.set(start, width);
- if (end > start + 1) {
- Arrays.fill(mWidths.getRawArray(), start + 1, end, 0.0f);
- }
- mWholeWidth += width;
- } else {
- nAddReplacementRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
- width);
- }
- }
-
- private void applyStyleRun(@IntRange(from = 0) int start, // inclusive, in copied buffer
- @IntRange(from = 0) int end, // exclusive, in copied buffer
- /* Maybe Zero */ long nativeBuilderPtr) {
- if (nativeBuilderPtr != 0) {
- mCachedPaint.getFontMetricsInt(mCachedFm);
+ /**
+ * Build the measured text
+ *
+ * @return the measured text.
+ */
+ public @NonNull MeasuredText build() {
+ return build(true /* build full layout result */);
}
- if (mLtrWithoutBidi) {
- // If the whole text is LTR direction, just apply whole region.
- if (nativeBuilderPtr == 0) {
- mWholeWidth += mCachedPaint.getTextRunAdvances(
- mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */,
- mWidths.getRawArray(), start);
- } else {
- nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
- false /* isRtl */);
- }
- } else {
- // If there is multiple bidi levels, split into individual bidi level and apply style.
- byte level = mLevels.get(start);
- // Note that the empty text or empty range won't reach this method.
- // Safe to search from start + 1.
- for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) {
- if (levelEnd == end || mLevels.get(levelEnd) != level) { // transition point
- final boolean isRtl = (level & 0x1) != 0;
- if (nativeBuilderPtr == 0) {
- final int levelLength = levelEnd - levelStart;
- mWholeWidth += mCachedPaint.getTextRunAdvances(
- mCopiedBuffer, levelStart, levelLength, levelStart, levelLength,
- isRtl, mWidths.getRawArray(), levelStart);
- } else {
- nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), levelStart,
- levelEnd, isRtl);
- }
- if (levelEnd == end) {
- break;
- }
- levelStart = levelEnd;
- level = mLevels.get(levelEnd);
- }
- }
- }
- }
+ /** @hide */
+ public @NonNull MeasuredText build(boolean computeLayout) {
+ final boolean needHyphenation = mBreakStrategy != Layout.BREAK_STRATEGY_SIMPLE
+ && mHyphenationFrequency != Layout.HYPHENATION_FREQUENCY_NONE;
- private void applyMetricsAffectingSpan(
- @NonNull TextPaint paint,
- @Nullable MetricAffectingSpan[] spans,
- @IntRange(from = 0) int start, // inclusive, in original text buffer
- @IntRange(from = 0) int end, // exclusive, in original text buffer
- /* Maybe Zero */ long nativeBuilderPtr) {
- mCachedPaint.set(paint);
- // XXX paint should not have a baseline shift, but...
- mCachedPaint.baselineShift = 0;
+ final IntArray paragraphEnds = new IntArray();
+ final ArrayList<MeasuredParagraph> measuredTexts = new ArrayList<>();
- final boolean needFontMetrics = nativeBuilderPtr != 0;
-
- if (needFontMetrics && mCachedFm == null) {
- mCachedFm = new Paint.FontMetricsInt();
- }
-
- ReplacementSpan replacement = null;
- if (spans != null) {
- for (int i = 0; i < spans.length; i++) {
- MetricAffectingSpan span = spans[i];
- if (span instanceof ReplacementSpan) {
- // The last ReplacementSpan is effective for backward compatibility reasons.
- replacement = (ReplacementSpan) span;
+ int paraEnd = 0;
+ for (int paraStart = mStart; paraStart < mEnd; paraStart = paraEnd) {
+ paraEnd = TextUtils.indexOf(mText, LINE_FEED, paraStart, mEnd);
+ if (paraEnd < 0) {
+ // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph
+ // end.
+ paraEnd = mEnd;
} else {
- // TODO: No need to call updateMeasureState for ReplacementSpan as well?
- span.updateMeasureState(mCachedPaint);
+ paraEnd++; // Includes LINE_FEED(U+000A) to the prev paragraph.
}
+
+ paragraphEnds.add(paraEnd);
+ measuredTexts.add(MeasuredParagraph.buildForStaticLayout(
+ mPaint, mText, paraStart, paraEnd, mTextDir, needHyphenation,
+ computeLayout, null /* no recycle */));
+ }
+
+ return new MeasuredText(mText, mStart, mEnd, mPaint, mTextDir, mBreakStrategy,
+ mHyphenationFrequency, measuredTexts.toArray(
+ new MeasuredParagraph[measuredTexts.size()]),
+ paragraphEnds.toArray());
+ }
+ };
+
+ // Use MeasuredText.Builder instead.
+ private MeasuredText(@NonNull CharSequence text,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @NonNull TextPaint paint,
+ @NonNull TextDirectionHeuristic textDir,
+ @Layout.BreakStrategy int breakStrategy,
+ @Layout.HyphenationFrequency int frequency,
+ @NonNull MeasuredParagraph[] measuredTexts,
+ @NonNull int[] paragraphBreakPoints) {
+ mText = text;
+ mStart = start;
+ mEnd = end;
+ // Copy the paint so that we can keep the reference of typeface in native layout result.
+ mPaint = new TextPaint(paint);
+ mMeasuredParagraphs = measuredTexts;
+ mParagraphBreakPoints = paragraphBreakPoints;
+ mTextDir = textDir;
+ mBreakStrategy = breakStrategy;
+ mHyphenationFrequency = frequency;
+ }
+
+ /**
+ * Return the underlying text.
+ */
+ public @NonNull CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Returns the inclusive start offset of measured region.
+ */
+ public @IntRange(from = 0) int getStart() {
+ return mStart;
+ }
+
+ /**
+ * Returns the exclusive end offset of measured region.
+ */
+ public @IntRange(from = 0) int getEnd() {
+ return mEnd;
+ }
+
+ /**
+ * Returns the text direction associated with char sequence.
+ */
+ public @NonNull TextDirectionHeuristic getTextDir() {
+ return mTextDir;
+ }
+
+ /**
+ * Returns the paint used to measure this text.
+ */
+ public @NonNull TextPaint getPaint() {
+ return mPaint;
+ }
+
+ /**
+ * Returns the length of the paragraph of this text.
+ */
+ public @IntRange(from = 0) int getParagraphCount() {
+ return mParagraphBreakPoints.length;
+ }
+
+ /**
+ * Returns the paragraph start offset of the text.
+ */
+ public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) {
+ Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
+ return paraIndex == 0 ? mStart : mParagraphBreakPoints[paraIndex - 1];
+ }
+
+ /**
+ * Returns the paragraph end offset of the text.
+ */
+ public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) {
+ Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
+ return mParagraphBreakPoints[paraIndex];
+ }
+
+ /** @hide */
+ public @NonNull MeasuredParagraph getMeasuredParagraph(@IntRange(from = 0) int paraIndex) {
+ return mMeasuredParagraphs[paraIndex];
+ }
+
+ /**
+ * Returns the break strategy for this text.
+ */
+ public @Layout.BreakStrategy int getBreakStrategy() {
+ return mBreakStrategy;
+ }
+
+ /**
+ * Returns the hyphenation frequency for this text.
+ */
+ public @Layout.HyphenationFrequency int getHyphenationFrequency() {
+ return mHyphenationFrequency;
+ }
+
+ /**
+ * Returns true if the given TextPaint gives the same result of text layout for this text.
+ * @hide
+ */
+ public boolean canUseMeasuredResult(@NonNull TextPaint paint) {
+ return mPaint.getTextSize() == paint.getTextSize()
+ && mPaint.getTextSkewX() == paint.getTextSkewX()
+ && mPaint.getTextScaleX() == paint.getTextScaleX()
+ && mPaint.getLetterSpacing() == paint.getLetterSpacing()
+ && mPaint.getWordSpacing() == paint.getWordSpacing()
+ && mPaint.getFlags() == paint.getFlags() // Maybe not all flag affects text layout.
+ && mPaint.getTextLocales() == paint.getTextLocales() // need to be equals?
+ && mPaint.getFontVariationSettings() == paint.getFontVariationSettings()
+ && mPaint.getTypeface() == paint.getTypeface()
+ && TextUtils.equals(mPaint.getFontFeatureSettings(), paint.getFontFeatureSettings());
+ }
+
+ /** @hide */
+ public int findParaIndex(@IntRange(from = 0) int pos) {
+ // TODO: Maybe good to remove paragraph concept from MeasuredText and add substring layout
+ // support to StaticLayout.
+ for (int i = 0; i < mParagraphBreakPoints.length; ++i) {
+ if (pos < mParagraphBreakPoints[i]) {
+ return i;
}
}
+ throw new IndexOutOfBoundsException(
+ "pos must be less than " + mParagraphBreakPoints[mParagraphBreakPoints.length - 1]
+ + ", gave " + pos);
+ }
- final int startInCopiedBuffer = start - mTextStart;
- final int endInCopiedBuffer = end - mTextStart;
+ /** @hide */
+ public float getWidth(@IntRange(from = 0) int start, @IntRange(from = 0) int end) {
+ final int paraIndex = findParaIndex(start);
+ final int paraStart = getParagraphStart(paraIndex);
+ final int paraEnd = getParagraphEnd(paraIndex);
+ if (start < paraStart || paraEnd < end) {
+ throw new RuntimeException("Cannot measured across the paragraph:"
+ + "para: (" + paraStart + ", " + paraEnd + "), "
+ + "request: (" + start + ", " + end + ")");
+ }
+ return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart);
+ }
- if (replacement != null) {
- applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer,
- nativeBuilderPtr);
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Spanned overrides
+ //
+ // Just proxy for underlying mText if appropriate.
+
+ @Override
+ public <T> T[] getSpans(int start, int end, Class<T> type) {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).getSpans(start, end, type);
} else {
- applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeBuilderPtr);
- }
-
- if (needFontMetrics) {
- if (mCachedPaint.baselineShift < 0) {
- mCachedFm.ascent += mCachedPaint.baselineShift;
- mCachedFm.top += mCachedPaint.baselineShift;
- } else {
- mCachedFm.descent += mCachedPaint.baselineShift;
- mCachedFm.bottom += mCachedPaint.baselineShift;
- }
-
- mFontMetrics.append(mCachedFm.top);
- mFontMetrics.append(mCachedFm.bottom);
- mFontMetrics.append(mCachedFm.ascent);
- mFontMetrics.append(mCachedFm.descent);
+ return ArrayUtils.emptyArray(type);
}
}
- /**
- * Returns the maximum index that the accumulated width not exceeds the width.
- *
- * If forward=false is passed, returns the minimum index from the end instead.
- *
- * This only works if the MeasuredText is computed with computeForMeasurement.
- * Undefined behavior in other case.
- */
- @IntRange(from = 0) int breakText(int limit, boolean forwards, float width) {
- float[] w = mWidths.getRawArray();
- if (forwards) {
- int i = 0;
- while (i < limit) {
- width -= w[i];
- if (width < 0.0f) break;
- i++;
- }
- while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--;
- return i;
+ @Override
+ public int getSpanStart(Object tag) {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).getSpanStart(tag);
} else {
- int i = limit - 1;
- while (i >= 0) {
- width -= w[i];
- if (width < 0.0f) break;
- i--;
- }
- while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) {
- i++;
- }
- return limit - i - 1;
+ return -1;
}
}
- /**
- * Returns the length of the substring.
- *
- * This only works if the MeasuredText is computed with computeForMeasurement.
- * Undefined behavior in other case.
- */
- @FloatRange(from = 0.0f) float measure(int start, int limit) {
- float width = 0;
- float[] w = mWidths.getRawArray();
- for (int i = start; i < limit; ++i) {
- width += w[i];
+ @Override
+ public int getSpanEnd(Object tag) {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).getSpanEnd(tag);
+ } else {
+ return -1;
}
- return width;
}
- private static native /* Non Zero */ long nInitBuilder();
+ @Override
+ public int getSpanFlags(Object tag) {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).getSpanFlags(tag);
+ } else {
+ return 0;
+ }
+ }
- /**
- * Apply style to make native measured text.
- *
- * @param nativeBuilderPtr The native MeasuredText builder pointer.
- * @param paintPtr The native paint pointer to be applied.
- * @param start The start offset in the copied buffer.
- * @param end The end offset in the copied buffer.
- * @param isRtl True if the text is RTL.
- */
- private static native void nAddStyleRun(/* Non Zero */ long nativeBuilderPtr,
- /* Non Zero */ long paintPtr,
- @IntRange(from = 0) int start,
- @IntRange(from = 0) int end,
- boolean isRtl);
+ @Override
+ public int nextSpanTransition(int start, int limit, Class type) {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).nextSpanTransition(start, limit, type);
+ } else {
+ return mText.length();
+ }
+ }
- /**
- * Apply ReplacementRun to make native measured text.
- *
- * @param nativeBuilderPtr The native MeasuredText builder pointer.
- * @param paintPtr The native paint pointer to be applied.
- * @param start The start offset in the copied buffer.
- * @param end The end offset in the copied buffer.
- * @param width The width of the replacement.
- */
- private static native void nAddReplacementRun(/* Non Zero */ long nativeBuilderPtr,
- /* Non Zero */ long paintPtr,
- @IntRange(from = 0) int start,
- @IntRange(from = 0) int end,
- @FloatRange(from = 0) float width);
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // CharSequence overrides.
+ //
+ // Just proxy for underlying mText.
- private static native long nBuildNativeMeasuredText(/* Non Zero */ long nativeBuilderPtr,
- @NonNull char[] text);
+ @Override
+ public int length() {
+ return mText.length();
+ }
- private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);
+ @Override
+ public char charAt(int index) {
+ // TODO: Should this be index + mStart ?
+ return mText.charAt(index);
+ }
- @CriticalNative
- private static native /* Non Zero */ long nGetReleaseFunc();
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ // TODO: return MeasuredText.
+ // TODO: Should this be index + mStart, end + mStart ?
+ return mText.subSequence(start, end);
+ }
+
+ @Override
+ public String toString() {
+ return mText.toString();
+ }
}
diff --git a/android/text/PremeasuredText.java b/android/text/PremeasuredText.java
deleted file mode 100644
index 465314d..0000000
--- a/android/text/PremeasuredText.java
+++ /dev/null
@@ -1,272 +0,0 @@
-/*
- * Copyright (C) 2017 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.text;
-
-import android.annotation.IntRange;
-import android.annotation.NonNull;
-import android.util.IntArray;
-
-import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.Preconditions;
-
-import java.util.ArrayList;
-
-/**
- * A text which has already been measured.
- *
- * TODO: Rename to better name? e.g. MeasuredText, FrozenText etc.
- */
-public class PremeasuredText implements Spanned {
- private static final char LINE_FEED = '\n';
-
- // The original text.
- private final @NonNull CharSequence mText;
-
- // The inclusive start offset of the measuring target.
- private final @IntRange(from = 0) int mStart;
-
- // The exclusive end offset of the measuring target.
- private final @IntRange(from = 0) int mEnd;
-
- // The TextPaint used for measurement.
- private final @NonNull TextPaint mPaint;
-
- // The requested text direction.
- private final @NonNull TextDirectionHeuristic mTextDir;
-
- // The measured paragraph texts.
- private final @NonNull MeasuredText[] mMeasuredTexts;
-
- // The sorted paragraph end offsets.
- private final @NonNull int[] mParagraphBreakPoints;
-
- /**
- * Build PremeasuredText from the text.
- *
- * @param text The text to be measured.
- * @param paint The paint to be used for drawing.
- * @param textDir The text direction.
- * @return The measured text.
- */
- public static @NonNull PremeasuredText build(@NonNull CharSequence text,
- @NonNull TextPaint paint,
- @NonNull TextDirectionHeuristic textDir) {
- return PremeasuredText.build(text, paint, textDir, 0, text.length());
- }
-
- /**
- * Build PremeasuredText from the specific range of the text..
- *
- * @param text The text to be measured.
- * @param paint The paint to be used for drawing.
- * @param textDir The text direction.
- * @param start The inclusive start offset of the text.
- * @param end The exclusive start offset of the text.
- * @return The measured text.
- */
- public static @NonNull PremeasuredText build(@NonNull CharSequence text,
- @NonNull TextPaint paint,
- @NonNull TextDirectionHeuristic textDir,
- @IntRange(from = 0) int start,
- @IntRange(from = 0) int end) {
- Preconditions.checkNotNull(text);
- Preconditions.checkNotNull(paint);
- Preconditions.checkNotNull(textDir);
- Preconditions.checkArgumentInRange(start, 0, text.length(), "start");
- Preconditions.checkArgumentInRange(end, 0, text.length(), "end");
-
- final IntArray paragraphEnds = new IntArray();
- final ArrayList<MeasuredText> measuredTexts = new ArrayList<>();
-
- int paraEnd = 0;
- for (int paraStart = start; paraStart < end; paraStart = paraEnd) {
- paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end);
- if (paraEnd < 0) {
- // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph end.
- paraEnd = end;
- } else {
- paraEnd++; // Includes LINE_FEED(U+000A) to the prev paragraph.
- }
-
- paragraphEnds.add(paraEnd);
- measuredTexts.add(MeasuredText.buildForStaticLayout(
- paint, text, paraStart, paraEnd, textDir, null /* no recycle */));
- }
-
- return new PremeasuredText(text, start, end, paint, textDir,
- measuredTexts.toArray(new MeasuredText[measuredTexts.size()]),
- paragraphEnds.toArray());
- }
-
- // Use PremeasuredText.build instead.
- private PremeasuredText(@NonNull CharSequence text,
- @IntRange(from = 0) int start,
- @IntRange(from = 0) int end,
- @NonNull TextPaint paint,
- @NonNull TextDirectionHeuristic textDir,
- @NonNull MeasuredText[] measuredTexts,
- @NonNull int[] paragraphBreakPoints) {
- mText = text;
- mStart = start;
- mEnd = end;
- mPaint = paint;
- mMeasuredTexts = measuredTexts;
- mParagraphBreakPoints = paragraphBreakPoints;
- mTextDir = textDir;
- }
-
- /**
- * Return the underlying text.
- */
- public @NonNull CharSequence getText() {
- return mText;
- }
-
- /**
- * Returns the inclusive start offset of measured region.
- */
- public @IntRange(from = 0) int getStart() {
- return mStart;
- }
-
- /**
- * Returns the exclusive end offset of measured region.
- */
- public @IntRange(from = 0) int getEnd() {
- return mEnd;
- }
-
- /**
- * Returns the text direction associated with char sequence.
- */
- public @NonNull TextDirectionHeuristic getTextDir() {
- return mTextDir;
- }
-
- /**
- * Returns the paint used to measure this text.
- */
- public @NonNull TextPaint getPaint() {
- return mPaint;
- }
-
- /**
- * Returns the length of the paragraph of this text.
- */
- public @IntRange(from = 0) int getParagraphCount() {
- return mParagraphBreakPoints.length;
- }
-
- /**
- * Returns the paragraph start offset of the text.
- */
- public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) {
- Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
- return paraIndex == 0 ? mStart : mParagraphBreakPoints[paraIndex - 1];
- }
-
- /**
- * Returns the paragraph end offset of the text.
- */
- public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) {
- Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
- return mParagraphBreakPoints[paraIndex];
- }
-
- /** @hide */
- public @NonNull MeasuredText getMeasuredText(@IntRange(from = 0) int paraIndex) {
- return mMeasuredTexts[paraIndex];
- }
-
- ///////////////////////////////////////////////////////////////////////////////////////////////
- // Spanned overrides
- //
- // Just proxy for underlying mText if appropriate.
-
- @Override
- public <T> T[] getSpans(int start, int end, Class<T> type) {
- if (mText instanceof Spanned) {
- return ((Spanned) mText).getSpans(start, end, type);
- } else {
- return ArrayUtils.emptyArray(type);
- }
- }
-
- @Override
- public int getSpanStart(Object tag) {
- if (mText instanceof Spanned) {
- return ((Spanned) mText).getSpanStart(tag);
- } else {
- return -1;
- }
- }
-
- @Override
- public int getSpanEnd(Object tag) {
- if (mText instanceof Spanned) {
- return ((Spanned) mText).getSpanEnd(tag);
- } else {
- return -1;
- }
- }
-
- @Override
- public int getSpanFlags(Object tag) {
- if (mText instanceof Spanned) {
- return ((Spanned) mText).getSpanFlags(tag);
- } else {
- return 0;
- }
- }
-
- @Override
- public int nextSpanTransition(int start, int limit, Class type) {
- if (mText instanceof Spanned) {
- return ((Spanned) mText).nextSpanTransition(start, limit, type);
- } else {
- return mText.length();
- }
- }
-
- ///////////////////////////////////////////////////////////////////////////////////////////////
- // CharSequence overrides.
- //
- // Just proxy for underlying mText.
-
- @Override
- public int length() {
- return mText.length();
- }
-
- @Override
- public char charAt(int index) {
- // TODO: Should this be index + mStart ?
- return mText.charAt(index);
- }
-
- @Override
- public CharSequence subSequence(int start, int end) {
- // TODO: return PremeasuredText.
- // TODO: Should this be index + mStart, end + mStart ?
- return mText.subSequence(start, end);
- }
-
- @Override
- public String toString() {
- return mText.toString();
- }
-}
diff --git a/android/text/StaticLayout.java b/android/text/StaticLayout.java
index d69b119..e62f421 100644
--- a/android/text/StaticLayout.java
+++ b/android/text/StaticLayout.java
@@ -55,7 +55,8 @@
* First, call nInit to setup native line breaker object. Then, for each paragraph, do the
* following:
*
- * - Create MeasuredText by MeasuredText.buildForStaticLayout which measures in native.
+ * - Create MeasuredParagraph by MeasuredParagraph.buildForStaticLayout which measures in
+ * native.
* - Run nComputeLineBreaks() to obtain line breaks for the paragraph.
*
* After all paragraphs, call finish() to release expensive buffers.
@@ -650,34 +651,48 @@
b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE,
indents, mLeftPaddings, mRightPaddings);
- PremeasuredText premeasured = null;
+ MeasuredText measured = null;
final Spanned spanned;
- if (source instanceof PremeasuredText) {
- premeasured = (PremeasuredText) source;
+ final boolean canUseMeasuredText;
+ if (source instanceof MeasuredText) {
+ measured = (MeasuredText) source;
- final CharSequence original = premeasured.getText();
- spanned = (original instanceof Spanned) ? (Spanned) original : null;
-
- if (bufStart != premeasured.getStart() || bufEnd != premeasured.getEnd()) {
+ if (bufStart != measured.getStart() || bufEnd != measured.getEnd()) {
// The buffer position has changed. Re-measure here.
- premeasured = PremeasuredText.build(original, paint, textDir, bufStart, bufEnd);
+ canUseMeasuredText = false;
+ } else if (b.mBreakStrategy != measured.getBreakStrategy()
+ || b.mHyphenationFrequency != measured.getHyphenationFrequency()) {
+ // The computed hyphenation pieces may not be able to used. Re-measure it.
+ canUseMeasuredText = false;
} else {
- // We can use premeasured information.
-
- // Overwrite with the one when premeasured.
- // TODO: Give an option for developer not to overwrite and measure again here?
- textDir = premeasured.getTextDir();
- paint = premeasured.getPaint();
+ // We can use measured information.
+ canUseMeasuredText = true;
}
} else {
- premeasured = PremeasuredText.build(source, paint, textDir, bufStart, bufEnd);
+ canUseMeasuredText = false;
+ }
+
+ if (!canUseMeasuredText) {
+ measured = new MeasuredText.Builder(source, paint)
+ .setRange(bufStart, bufEnd)
+ .setTextDirection(textDir)
+ .setBreakStrategy(b.mBreakStrategy)
+ .setHyphenationFrequency(b.mHyphenationFrequency)
+ .build(false /* full layout is not necessary for line breaking */);
spanned = (source instanceof Spanned) ? (Spanned) source : null;
+ } else {
+ final CharSequence original = measured.getText();
+ spanned = (original instanceof Spanned) ? (Spanned) original : null;
+ // Overwrite with the one when measured.
+ // TODO: Give an option for developer not to overwrite and measure again here?
+ textDir = measured.getTextDir();
+ paint = measured.getPaint();
}
try {
- for (int paraIndex = 0; paraIndex < premeasured.getParagraphCount(); paraIndex++) {
- final int paraStart = premeasured.getParagraphStart(paraIndex);
- final int paraEnd = premeasured.getParagraphEnd(paraIndex);
+ for (int paraIndex = 0; paraIndex < measured.getParagraphCount(); paraIndex++) {
+ final int paraStart = measured.getParagraphStart(paraIndex);
+ final int paraEnd = measured.getParagraphEnd(paraIndex);
int firstWidthLineCount = 1;
int firstWidth = outerWidth;
@@ -743,10 +758,10 @@
}
}
- final MeasuredText measured = premeasured.getMeasuredText(paraIndex);
- final char[] chs = measured.getChars();
- final int[] spanEndCache = measured.getSpanEndCache().getRawArray();
- final int[] fmCache = measured.getFontMetrics().getRawArray();
+ final MeasuredParagraph measuredPara = measured.getMeasuredParagraph(paraIndex);
+ final char[] chs = measuredPara.getChars();
+ final int[] spanEndCache = measuredPara.getSpanEndCache().getRawArray();
+ final int[] fmCache = measuredPara.getFontMetrics().getRawArray();
// TODO: Stop keeping duplicated width copy in native and Java.
widths.resize(chs.length);
@@ -759,7 +774,7 @@
// Inputs
chs,
- measured.getNativePtr(),
+ measuredPara.getNativePtr(),
paraEnd - paraStart,
firstWidth,
firstWidthLineCount,
@@ -863,7 +878,7 @@
v = out(source, here, endPos,
ascent, descent, fmTop, fmBottom,
v, spacingmult, spacingadd, chooseHt, chooseHtv, fm,
- flags[breakIndex], needMultiply, measured, bufEnd,
+ flags[breakIndex], needMultiply, measuredPara, bufEnd,
includepad, trackpad, addLastLineSpacing, chs, widths.getRawArray(),
paraStart, ellipsize, ellipsizedWidth, lineWidths[breakIndex],
paint, moreChars);
@@ -894,8 +909,8 @@
if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE)
&& mLineCount < mMaximumVisibleLineCount) {
- final MeasuredText measured =
- MeasuredText.buildForBidi(source, bufEnd, bufEnd, textDir, null);
+ final MeasuredParagraph measuredPara =
+ MeasuredParagraph.buildForBidi(source, bufEnd, bufEnd, textDir, null);
paint.getFontMetricsInt(fm);
v = out(source,
bufEnd, bufEnd, fm.ascent, fm.descent,
@@ -903,7 +918,7 @@
v,
spacingmult, spacingadd, null,
null, fm, 0,
- needMultiply, measured, bufEnd,
+ needMultiply, measuredPara, bufEnd,
includepad, trackpad, addLastLineSpacing, null,
null, bufStart, ellipsize,
ellipsizedWidth, 0, paint, false);
@@ -913,12 +928,10 @@
}
}
- // The parameters that are not changed in the method are marked as final to make the code
- // easier to understand.
private int out(final CharSequence text, final int start, final int end, int above, int below,
int top, int bottom, int v, final float spacingmult, final float spacingadd,
final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm,
- final int flags, final boolean needMultiply, @NonNull final MeasuredText measured,
+ final int flags, final boolean needMultiply, @NonNull final MeasuredParagraph measured,
final int bufEnd, final boolean includePad, final boolean trackPad,
final boolean addLastLineLineSpacing, final char[] chs, final float[] widths,
final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth,
@@ -943,21 +956,29 @@
mLineDirections = grow;
}
- lines[off + START] = start;
- lines[off + TOP] = v;
+ if (chooseHt != null) {
+ fm.ascent = above;
+ fm.descent = below;
+ fm.top = top;
+ fm.bottom = bottom;
- // Information about hyphenation, tabs, and directions are needed for determining
- // ellipsization, so the values should be assigned before ellipsization.
+ for (int i = 0; i < chooseHt.length; i++) {
+ if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
+ ((LineHeightSpan.WithDensity) chooseHt[i])
+ .chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
+ } else {
+ chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
+ }
+ }
- // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining
- // one bit for start field
- lines[off + TAB] |= flags & TAB_MASK;
- lines[off + HYPHEN] = flags;
- lines[off + DIR] |= dir << DIR_SHIFT;
- mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
+ above = fm.ascent;
+ below = fm.descent;
+ top = fm.top;
+ bottom = fm.bottom;
+ }
- final boolean firstLine = (j == 0);
- final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
+ boolean firstLine = (j == 0);
+ boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
if (ellipsize != null) {
// If there is only one line, then do any type of ellipsis except when it is MARQUEE
@@ -970,9 +991,9 @@
(!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
ellipsize == TextUtils.TruncateAt.END);
if (doEllipsis) {
- calculateEllipsis(text, start, end, widths, widthStart,
- ellipsisWidth - getTotalInsets(j), ellipsize, j,
- textWidth, paint, forceEllipsis, dir);
+ calculateEllipsis(start, end, widths, widthStart,
+ ellipsisWidth, ellipsize, j,
+ textWidth, paint, forceEllipsis);
}
}
@@ -991,28 +1012,6 @@
}
}
- if (chooseHt != null) {
- fm.ascent = above;
- fm.descent = below;
- fm.top = top;
- fm.bottom = bottom;
-
- for (int i = 0; i < chooseHt.length; i++) {
- if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
- ((LineHeightSpan.WithDensity) chooseHt[i])
- .chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
-
- } else {
- chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
- }
- }
-
- above = fm.ascent;
- below = fm.descent;
- top = fm.top;
- bottom = fm.bottom;
- }
-
if (firstLine) {
if (trackPad) {
mTopPadding = top - above;
@@ -1023,6 +1022,8 @@
}
}
+ int extra;
+
if (lastLine) {
if (trackPad) {
mBottomPadding = bottom - below;
@@ -1033,9 +1034,8 @@
}
}
- final int extra;
if (needMultiply && (addLastLineLineSpacing || !lastLine)) {
- final double ex = (below - above) * (spacingmult - 1) + spacingadd;
+ double ex = (below - above) * (spacingmult - 1) + spacingadd;
if (ex >= 0) {
extra = (int)(ex + EXTRA_ROUNDING);
} else {
@@ -1045,6 +1045,8 @@
extra = 0;
}
+ lines[off + START] = start;
+ lines[off + TOP] = v;
lines[off + DESCENT] = below + extra;
lines[off + EXTRA] = extra;
@@ -1052,7 +1054,7 @@
// store the height as if it was ellipsized
if (!mEllipsized && currentLineIsTheLastVisibleOne) {
// below calculation as if it was the last line
- final int maxLineBelow = includePad ? bottom : below;
+ int maxLineBelow = includePad ? bottom : below;
// similar to the calculation of v below, without the extra.
mMaxLineHeight = v + (maxLineBelow - above);
}
@@ -1061,13 +1063,23 @@
lines[off + mColumns + START] = end;
lines[off + mColumns + TOP] = v;
+ // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining
+ // one bit for start field
+ lines[off + TAB] |= flags & TAB_MASK;
+ lines[off + HYPHEN] = flags;
+ lines[off + DIR] |= dir << DIR_SHIFT;
+ mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
+
mLineCount++;
return v;
}
- private void calculateEllipsis(CharSequence text, int lineStart, int lineEnd, float[] widths,
- int widthStart, float avail, TextUtils.TruncateAt where, int line, float textWidth,
- TextPaint paint, boolean forceEllipsis, int dir) {
+ private void calculateEllipsis(int lineStart, int lineEnd,
+ float[] widths, int widthStart,
+ float avail, TextUtils.TruncateAt where,
+ int line, float textWidth, TextPaint paint,
+ boolean forceEllipsis) {
+ avail -= getTotalInsets(line);
if (textWidth <= avail && !forceEllipsis) {
// Everything fits!
mLines[mColumns * line + ELLIPSIS_START] = 0;
@@ -1075,53 +1087,11 @@
return;
}
- float tempAvail = avail;
- int numberOfTries = 0;
- boolean lineFits = false;
- mWorkPaint.set(paint);
- do {
- final float ellipsizedWidth = guessEllipsis(text, lineStart, lineEnd, widths,
- widthStart, tempAvail, where, line, mWorkPaint, forceEllipsis, dir);
- if (ellipsizedWidth <= avail) {
- lineFits = true;
- } else {
- numberOfTries++;
- if (numberOfTries > 10) {
- // If the text still doesn't fit after ten tries, assume it will never fit and
- // ellipsize it all.
- mLines[mColumns * line + ELLIPSIS_START] = 0;
- mLines[mColumns * line + ELLIPSIS_COUNT] = lineEnd - lineStart;
- lineFits = true;
- } else {
- // Some side effect of ellipsization has caused the text to go over the
- // available width. Let's make the available width shorter by exactly that
- // amount and retry.
- tempAvail -= ellipsizedWidth - avail;
- }
- }
- } while (!lineFits);
- mEllipsized = true;
- }
+ float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where));
+ int ellipsisStart = 0;
+ int ellipsisCount = 0;
+ int len = lineEnd - lineStart;
- // Returns the width of the ellipsized line which in some rare cases can actually be larger
- // than 'avail' (due to kerning or other context-based effect of replacement of text by
- // ellipsis). If all the line needs to ellipsized away, or it's an invalud hyphenation mode,
- // returns 0 so the caller can stop iterating.
- //
- // This method temporarily modifies the TextPaint passed to it, so the TextPaint passed to it
- // should not be accessed while the method is running.
- private float guessEllipsis(CharSequence text, int lineStart, int lineEnd, float[] widths,
- int widthStart, float avail, TextUtils.TruncateAt where, int line,
- TextPaint paint, boolean forceEllipsis, int dir) {
- final int savedHyphenEdit = paint.getHyphenEdit();
- paint.setHyphenEdit(0);
- final float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where));
- final int ellipsisStart;
- final int ellipsisCount;
- final int len = lineEnd - lineStart;
- final int offset = lineStart - widthStart;
-
- int hyphen = getHyphen(line);
// We only support start ellipsis on a single line
if (where == TextUtils.TruncateAt.START) {
if (mMaximumVisibleLineCount == 1) {
@@ -1129,9 +1099,9 @@
int i;
for (i = len; i > 0; i--) {
- final float w = widths[i - 1 + offset];
+ float w = widths[i - 1 + lineStart - widthStart];
if (w + sum + ellipsisWidth > avail) {
- while (i < len && widths[i + offset] == 0.0f) {
+ while (i < len && widths[i + lineStart - widthStart] == 0.0f) {
i++;
}
break;
@@ -1142,13 +1112,9 @@
ellipsisStart = 0;
ellipsisCount = i;
- // Strip the potential hyphenation at beginning of line.
- hyphen &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE;
} else {
- ellipsisStart = 0;
- ellipsisCount = 0;
if (Log.isLoggable(TAG, Log.WARN)) {
- Log.w(TAG, "Start ellipsis only supported with one line");
+ Log.w(TAG, "Start Ellipsis only supported with one line");
}
}
} else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
@@ -1157,7 +1123,7 @@
int i;
for (i = 0; i < len; i++) {
- final float w = widths[i + offset];
+ float w = widths[i + lineStart - widthStart];
if (w + sum + ellipsisWidth > avail) {
break;
@@ -1166,27 +1132,24 @@
sum += w;
}
- if (forceEllipsis && i == len && len > 0) {
+ ellipsisStart = i;
+ ellipsisCount = len - i;
+ if (forceEllipsis && ellipsisCount == 0 && len > 0) {
ellipsisStart = len - 1;
ellipsisCount = 1;
- } else {
- ellipsisStart = i;
- ellipsisCount = len - i;
}
- // Strip the potential hyphenation at end of line.
- hyphen &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE;
- } else { // where = TextUtils.TruncateAt.MIDDLE
- // We only support middle ellipsis on a single line.
+ } else {
+ // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
if (mMaximumVisibleLineCount == 1) {
float lsum = 0, rsum = 0;
int left = 0, right = len;
- final float ravail = (avail - ellipsisWidth) / 2;
+ float ravail = (avail - ellipsisWidth) / 2;
for (right = len; right > 0; right--) {
- final float w = widths[right - 1 + offset];
+ float w = widths[right - 1 + lineStart - widthStart];
if (w + rsum > ravail) {
- while (right < len && widths[right + offset] == 0.0f) {
+ while (right < len && widths[right + lineStart - widthStart] == 0.0f) {
right++;
}
break;
@@ -1194,9 +1157,9 @@
rsum += w;
}
- final float lavail = avail - ellipsisWidth - rsum;
+ float lavail = avail - ellipsisWidth - rsum;
for (left = 0; left < right; left++) {
- final float w = widths[left + offset];
+ float w = widths[left + lineStart - widthStart];
if (w + lsum > lavail) {
break;
@@ -1208,53 +1171,14 @@
ellipsisStart = left;
ellipsisCount = right - left;
} else {
- ellipsisStart = 0;
- ellipsisCount = 0;
if (Log.isLoggable(TAG, Log.WARN)) {
- Log.w(TAG, "Middle ellipsis only supported with one line");
+ Log.w(TAG, "Middle Ellipsis only supported with one line");
}
}
}
+ mEllipsized = true;
mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
-
- if (ellipsisStart == 0 && (ellipsisCount == 0 || ellipsisCount == len)) {
- // Unsupported ellipsization mode or all text is ellipsized away. Return 0.
- return 0.0f;
- }
-
- final boolean isSpanned = text instanceof Spanned;
- final Ellipsizer ellipsizedText = isSpanned
- ? new SpannedEllipsizer(text)
- : new Ellipsizer(text);
- ellipsizedText.mLayout = this;
- ellipsizedText.mMethod = where;
-
- final boolean hasTabs = getLineContainsTab(line);
- final TabStops tabStops;
- if (hasTabs && isSpanned) {
- final TabStopSpan[] tabs = getParagraphSpans((Spanned) ellipsizedText, lineStart,
- lineEnd, TabStopSpan.class);
- if (tabs.length == 0) {
- tabStops = null;
- } else {
- tabStops = new TabStops(TAB_INCREMENT, tabs);
- }
- } else {
- tabStops = null;
- }
- paint.setHyphenEdit(hyphen);
- final TextLine textline = TextLine.obtain();
- textline.set(paint, ellipsizedText, lineStart, lineEnd, dir, getLineDirections(line),
- hasTabs, tabStops);
- // Since TextLine.metric() returns negative values for RTL text, multiplication by dir
- // converts it to an actual width. Note that we don't want to use the absolute value,
- // since we may actually have glyphs with negative advances, which by definition always
- // fit.
- final float ellipsizedWidth = textline.metrics(null) * dir;
- TextLine.recycle(textline);
- paint.setHyphenEdit(savedHyphenEdit);
- return ellipsizedWidth;
}
private float getTotalInsets(int line) {
@@ -1494,8 +1418,6 @@
*/
private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT;
- private TextPaint mWorkPaint = new TextPaint();
-
private static final int COLUMNS_NORMAL = 5;
private static final int COLUMNS_ELLIPSIZE = 7;
private static final int START = 0;
diff --git a/android/text/StaticLayoutPerfTest.java b/android/text/StaticLayoutPerfTest.java
index 5653a03..682885b 100644
--- a/android/text/StaticLayoutPerfTest.java
+++ b/android/text/StaticLayoutPerfTest.java
@@ -25,10 +25,14 @@
import android.support.test.runner.AndroidJUnit4;
import android.content.res.ColorStateList;
+import android.graphics.Canvas;
import android.graphics.Typeface;
import android.text.Layout;
import android.text.style.TextAppearanceSpan;
+import android.view.DisplayListCanvas;
+import android.view.RenderNode;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -52,7 +56,7 @@
private static final boolean NO_STYLE_TEXT = false;
private static final boolean STYLE_TEXT = true;
- private final Random mRandom = new Random(31415926535L);
+ private Random mRandom;
private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private static final int ALPHABET_LENGTH = ALPHABET.length();
@@ -98,6 +102,11 @@
return ssb;
}
+ @Before
+ public void setUp() {
+ mRandom = new Random(0);
+ }
+
@Test
public void testCreate_FixedText_NoStyle_Greedy_NoHyphenation() {
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
@@ -190,8 +199,11 @@
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
state.pauseTiming();
- final PremeasuredText text = PremeasuredText.build(
- generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
+ .build();
state.resumeTiming();
StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
@@ -206,8 +218,11 @@
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
state.pauseTiming();
- final PremeasuredText text = PremeasuredText.build(
- generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+ .build();
state.resumeTiming();
StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
@@ -222,8 +237,11 @@
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
state.pauseTiming();
- final PremeasuredText text = PremeasuredText.build(
- generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
+ .build();
state.resumeTiming();
StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
@@ -238,8 +256,11 @@
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
state.pauseTiming();
- final PremeasuredText text = PremeasuredText.build(
- generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+ .build();
state.resumeTiming();
StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
@@ -254,8 +275,11 @@
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
state.pauseTiming();
- final PremeasuredText text = PremeasuredText.build(
- generateRandomParagraph(WORD_LENGTH, STYLE_TEXT), PAINT, LTR);
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
+ .build();
state.resumeTiming();
StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
@@ -264,4 +288,157 @@
.build();
}
}
+
+ @Test
+ public void testDraw_FixedText_NoStyled() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final CharSequence text = generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT);
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
+ @Test
+ public void testDraw_RandomText_Styled() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final CharSequence text = generateRandomParagraph(WORD_LENGTH, STYLE_TEXT);
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
+ @Test
+ public void testDraw_RandomText_NoStyled() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final CharSequence text = generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT);
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
+ @Test
+ public void testDraw_RandomText_Styled_WithoutCache() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final CharSequence text = generateRandomParagraph(WORD_LENGTH, STYLE_TEXT);
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ Canvas.freeTextLayoutCaches();
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
+ @Test
+ public void testDraw_RandomText_NoStyled_WithoutCache() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final CharSequence text = generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT);
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ Canvas.freeTextLayoutCaches();
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
+ @Test
+ public void testDraw_MeasuredText_Styled() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, STYLE_TEXT), PAINT).build();
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
+ @Test
+ public void testDraw_MeasuredText_NoStyled() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT).build();
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
+ @Test
+ public void testDraw_MeasuredText_Styled_WithoutCache() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, STYLE_TEXT), PAINT).build();
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ Canvas.freeTextLayoutCaches();
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
+ @Test
+ public void testDraw_MeasuredText_NoStyled_WithoutCache() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT).build();
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ Canvas.freeTextLayoutCaches();
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
}
diff --git a/android/text/StaticLayout_Delegate.java b/android/text/StaticLayout_Delegate.java
index d524954..d7cb596 100644
--- a/android/text/StaticLayout_Delegate.java
+++ b/android/text/StaticLayout_Delegate.java
@@ -87,7 +87,7 @@
builder.mLineWidth = new LineWidth(firstWidth, firstWidthLineCount, restWidth);
builder.mTabStopCalculator = new TabStops(variableTabStops, defaultTabStop);
- MeasuredText_Delegate.computeRuns(measuredTextPtr, builder);
+ MeasuredParagraph_Delegate.computeRuns(measuredTextPtr, builder);
// compute all possible breakpoints.
BreakIterator it = BreakIterator.getLineInstance();
diff --git a/android/text/TextLine.java b/android/text/TextLine.java
index 86cc014..55367dc 100644
--- a/android/text/TextLine.java
+++ b/android/text/TextLine.java
@@ -60,6 +60,7 @@
private char[] mChars;
private boolean mCharsValid;
private Spanned mSpanned;
+ private MeasuredText mMeasured;
// Additional width of whitespace for justification. This value is per whitespace, thus
// the line width will increase by mAddedWidth x (number of stretchable whitespaces).
@@ -118,6 +119,7 @@
tl.mSpanned = null;
tl.mTabs = null;
tl.mChars = null;
+ tl.mMeasured = null;
tl.mMetricAffectingSpanSpanSet.recycle();
tl.mCharacterStyleSpanSet.recycle();
@@ -168,6 +170,14 @@
hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
}
+ mMeasured = null;
+ if (text instanceof MeasuredText) {
+ MeasuredText mt = (MeasuredText) text;
+ if (mt.canUseMeasuredResult(paint)) {
+ mMeasured = mt;
+ }
+ }
+
mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT;
if (mCharsValid) {
@@ -736,8 +746,13 @@
return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset);
} else {
final int delta = mStart;
- return wp.getRunAdvance(mText, delta + start, delta + end,
- delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
+ if (mMeasured == null) {
+ // TODO: Enable measured getRunAdvance for ReplacementSpan and RTL text.
+ return wp.getRunAdvance(mText, delta + start, delta + end,
+ delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
+ } else {
+ return mMeasured.getWidth(start + delta, end + delta);
+ }
}
}
diff --git a/android/text/TextUtils.java b/android/text/TextUtils.java
index 9c9fbf2..af0eebf 100644
--- a/android/text/TextUtils.java
+++ b/android/text/TextUtils.java
@@ -88,8 +88,8 @@
/** {@hide} */
@NonNull
- public static String getEllipsisString(@NonNull TruncateAt method) {
- return (method == TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL;
+ public static String getEllipsisString(@NonNull TextUtils.TruncateAt method) {
+ return (method == TextUtils.TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL;
}
@@ -1194,11 +1194,9 @@
* or, if it does not fit, a truncated
* copy with ellipsis character added at the specified edge or center.
*/
- @NonNull
- public static CharSequence ellipsize(@NonNull CharSequence text,
- @NonNull TextPaint p,
- @FloatRange(from = 0.0) float avail,
- @NonNull TruncateAt where) {
+ public static CharSequence ellipsize(CharSequence text,
+ TextPaint p,
+ float avail, TruncateAt where) {
return ellipsize(text, p, avail, where, false, null);
}
@@ -1214,11 +1212,9 @@
* report the start and end of the ellipsized range. TextDirection
* is determined by the first strong directional character.
*/
- @NonNull
- public static CharSequence ellipsize(@NonNull CharSequence text,
- @NonNull TextPaint paint,
- @FloatRange(from = 0.0) float avail,
- @NonNull TruncateAt where,
+ public static CharSequence ellipsize(CharSequence text,
+ TextPaint paint,
+ float avail, TruncateAt where,
boolean preserveLength,
@Nullable EllipsizeCallback callback) {
return ellipsize(text, paint, avail, where, preserveLength, callback,
@@ -1239,131 +1235,94 @@
*
* @hide
*/
- @NonNull
- public static CharSequence ellipsize(@NonNull CharSequence text,
- @NonNull TextPaint paint,
- @FloatRange(from = 0.0) float avail,
- @NonNull TruncateAt where,
+ public static CharSequence ellipsize(CharSequence text,
+ TextPaint paint,
+ float avail, TruncateAt where,
boolean preserveLength,
@Nullable EllipsizeCallback callback,
- @NonNull TextDirectionHeuristic textDir,
- @NonNull String ellipsis) {
+ TextDirectionHeuristic textDir, String ellipsis) {
- final int len = text.length();
- MeasuredText mt = null;
- MeasuredText resultMt = null;
+ int len = text.length();
+
+ MeasuredParagraph mt = null;
try {
- mt = MeasuredText.buildForMeasurement(paint, text, 0, text.length(), textDir, mt);
+ mt = MeasuredParagraph.buildForMeasurement(paint, text, 0, text.length(), textDir, mt);
float width = mt.getWholeWidth();
if (width <= avail) {
if (callback != null) {
callback.ellipsized(0, 0);
}
+
return text;
}
- // First estimate of effective width of ellipsis.
- float ellipsisWidth = paint.measureText(ellipsis);
- int numberOfTries = 0;
- boolean textFits = false;
- int start, end;
- CharSequence result;
- do {
- if (avail < ellipsisWidth) {
- // Even the ellipsis can't fit. So it all goes.
- start = 0;
- end = len;
- } else {
- final float remainingWidth = avail - ellipsisWidth;
- if (where == TruncateAt.START) {
- start = 0;
- end = len - mt.breakText(len, false /* backwards */, remainingWidth);
- } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) {
- start = mt.breakText(len, true /* forwards */, remainingWidth);
- end = len;
- } else {
- end = len - mt.breakText(len, false /* backwards */, remainingWidth / 2);
- start = mt.breakText(end, true /* forwards */,
- remainingWidth - mt.measure(end, len));
- }
- }
+ // XXX assumes ellipsis string does not require shaping and
+ // is unaffected by style
+ float ellipsiswid = paint.measureText(ellipsis);
+ avail -= ellipsiswid;
- final char[] buf = mt.getChars();
- final Spanned sp = text instanceof Spanned ? (Spanned) text : null;
-
- final int removed = end - start;
- final int remaining = len - removed;
- if (preserveLength) {
- int pos = start;
- if (remaining > 0 && removed >= ellipsis.length()) {
- ellipsis.getChars(0, ellipsis.length(), buf, start);
- pos += ellipsis.length();
- } // else eliminate the ellipsis
- while (pos < end) {
- buf[pos++] = ELLIPSIS_FILLER;
- }
- final String s = new String(buf, 0, len);
- if (sp == null) {
- result = s;
- } else {
- final SpannableString ss = new SpannableString(s);
- copySpansFrom(sp, 0, len, Object.class, ss, 0);
- result = ss;
- }
- } else {
- if (remaining == 0) {
- result = "";
- } else if (sp == null) {
- final StringBuilder sb = new StringBuilder(remaining + ellipsis.length());
- sb.append(buf, 0, start);
- sb.append(ellipsis);
- sb.append(buf, end, len - end);
- result = sb.toString();
- } else {
- final SpannableStringBuilder ssb = new SpannableStringBuilder();
- ssb.append(text, 0, start);
- ssb.append(ellipsis);
- ssb.append(text, end, len);
- result = ssb;
- }
- }
-
- if (remaining == 0) { // All text is gone.
- textFits = true;
- } else {
- resultMt = MeasuredText.buildForMeasurement(
- paint, result, 0, result.length(), textDir, resultMt);
- width = resultMt.getWholeWidth();
- if (width <= avail) {
- textFits = true;
- } else {
- numberOfTries++;
- if (numberOfTries > 10) {
- // If the text still doesn't fit after ten tries, assume it will never
- // fit and ellipsize it all. We do this by setting the width of the
- // ellipsis to be positive infinity, so we get to empty text in the next
- // round.
- ellipsisWidth = Float.POSITIVE_INFINITY;
- } else {
- // Adjust the width of the ellipsis by adding the amount 'width' is
- // still over.
- ellipsisWidth += width - avail;
- }
- }
- }
- } while (!textFits);
- if (callback != null) {
- callback.ellipsized(start, end);
+ int left = 0;
+ int right = len;
+ if (avail < 0) {
+ // it all goes
+ } else if (where == TruncateAt.START) {
+ right = len - mt.breakText(len, false, avail);
+ } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) {
+ left = mt.breakText(len, true, avail);
+ } else {
+ right = len - mt.breakText(len, false, avail / 2);
+ avail -= mt.measure(right, len);
+ left = mt.breakText(right, true, avail);
}
- return result;
+
+ if (callback != null) {
+ callback.ellipsized(left, right);
+ }
+
+ final char[] buf = mt.getChars();
+ Spanned sp = text instanceof Spanned ? (Spanned) text : null;
+
+ final int removed = right - left;
+ final int remaining = len - removed;
+ if (preserveLength) {
+ if (remaining > 0 && removed >= ellipsis.length()) {
+ ellipsis.getChars(0, ellipsis.length(), buf, left);
+ left += ellipsis.length();
+ } // else skip the ellipsis
+ for (int i = left; i < right; i++) {
+ buf[i] = ELLIPSIS_FILLER;
+ }
+ String s = new String(buf, 0, len);
+ if (sp == null) {
+ return s;
+ }
+ SpannableString ss = new SpannableString(s);
+ copySpansFrom(sp, 0, len, Object.class, ss, 0);
+ return ss;
+ }
+
+ if (remaining == 0) {
+ return "";
+ }
+
+ if (sp == null) {
+ StringBuilder sb = new StringBuilder(remaining + ellipsis.length());
+ sb.append(buf, 0, left);
+ sb.append(ellipsis);
+ sb.append(buf, right, len - right);
+ return sb.toString();
+ }
+
+ SpannableStringBuilder ssb = new SpannableStringBuilder();
+ ssb.append(text, 0, left);
+ ssb.append(ellipsis);
+ ssb.append(text, right, len);
+ return ssb;
} finally {
if (mt != null) {
mt.recycle();
}
- if (resultMt != null) {
- resultMt.recycle();
- }
}
}
@@ -1394,6 +1353,7 @@
* @return the formatted CharSequence. If even the shortest sequence (e.g. {@code "A, 11 more"})
* doesn't fit, it will return an empty string.
*/
+
public static CharSequence listEllipsize(@Nullable Context context,
@Nullable List<CharSequence> elements, @NonNull String separator,
@NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail,
@@ -1479,11 +1439,11 @@
public static CharSequence commaEllipsize(CharSequence text, TextPaint p,
float avail, String oneMore, String more, TextDirectionHeuristic textDir) {
- MeasuredText mt = null;
- MeasuredText tempMt = null;
+ MeasuredParagraph mt = null;
+ MeasuredParagraph tempMt = null;
try {
int len = text.length();
- mt = MeasuredText.buildForMeasurement(p, text, 0, len, textDir, mt);
+ mt = MeasuredParagraph.buildForMeasurement(p, text, 0, len, textDir, mt);
final float width = mt.getWholeWidth();
if (width <= avail) {
return text;
@@ -1523,7 +1483,7 @@
}
// XXX this is probably ok, but need to look at it more
- tempMt = MeasuredText.buildForMeasurement(
+ tempMt = MeasuredParagraph.buildForMeasurement(
p, format, 0, format.length(), textDir, tempMt);
float moreWid = tempMt.getWholeWidth();
diff --git a/android/text/format/Formatter.java b/android/text/format/Formatter.java
index 8c90156..ad3b4b6 100644
--- a/android/text/format/Formatter.java
+++ b/android/text/format/Formatter.java
@@ -20,11 +20,7 @@
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
-import android.icu.text.DecimalFormat;
import android.icu.text.MeasureFormat;
-import android.icu.text.NumberFormat;
-import android.icu.text.UnicodeSet;
-import android.icu.text.UnicodeSetSpanner;
import android.icu.util.Measure;
import android.icu.util.MeasureUnit;
import android.net.NetworkUtils;
@@ -32,8 +28,6 @@
import android.text.TextUtils;
import android.view.View;
-import java.lang.reflect.Constructor;
-import java.math.BigDecimal;
import java.util.Locale;
/**
@@ -43,8 +37,6 @@
public final class Formatter {
/** {@hide} */
- public static final int FLAG_DEFAULT = 0;
- /** {@hide} */
public static final int FLAG_SHORTER = 1 << 0;
/** {@hide} */
public static final int FLAG_CALCULATE_ROUNDED = 1 << 1;
@@ -66,9 +58,7 @@
return context.getResources().getConfiguration().getLocales().get(0);
}
- /**
- * Wraps the source string in bidi formatting characters in RTL locales.
- */
+ /* Wraps the source string in bidi formatting characters in RTL locales */
private static String bidiWrap(@NonNull Context context, String source) {
final Locale locale = localeFromContext(context);
if (TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL) {
@@ -97,7 +87,12 @@
* @return formatted string with the number
*/
public static String formatFileSize(@Nullable Context context, long sizeBytes) {
- return formatFileSize(context, sizeBytes, FLAG_DEFAULT);
+ if (context == null) {
+ return "";
+ }
+ final BytesResult res = formatBytes(context.getResources(), sizeBytes, 0);
+ return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix,
+ res.value, res.units));
}
/**
@@ -105,207 +100,88 @@
* (showing fewer digits of precision).
*/
public static String formatShortFileSize(@Nullable Context context, long sizeBytes) {
- return formatFileSize(context, sizeBytes, FLAG_SHORTER);
- }
-
- private static String formatFileSize(@Nullable Context context, long sizeBytes, int flags) {
if (context == null) {
return "";
}
- final RoundedBytesResult res = RoundedBytesResult.roundBytes(sizeBytes, flags);
- return bidiWrap(context, formatRoundedBytesResult(context, res));
- }
-
- private static String getSuffixOverride(@NonNull Resources res, MeasureUnit unit) {
- if (unit == MeasureUnit.BYTE) {
- return res.getString(com.android.internal.R.string.byteShort);
- } else { // unit == PETABYTE
- return res.getString(com.android.internal.R.string.petabyteShort);
- }
- }
-
- private static NumberFormat getNumberFormatter(Locale locale, int fractionDigits) {
- final NumberFormat numberFormatter = NumberFormat.getInstance(locale);
- numberFormatter.setMinimumFractionDigits(fractionDigits);
- numberFormatter.setMaximumFractionDigits(fractionDigits);
- numberFormatter.setGroupingUsed(false);
- if (numberFormatter instanceof DecimalFormat) {
- // We do this only for DecimalFormat, since in the general NumberFormat case, calling
- // setRoundingMode may throw an exception.
- numberFormatter.setRoundingMode(BigDecimal.ROUND_HALF_UP);
- }
- return numberFormatter;
- }
-
- private static String deleteFirstFromString(String source, String toDelete) {
- final int location = source.indexOf(toDelete);
- if (location == -1) {
- return source;
- } else {
- return source.substring(0, location)
- + source.substring(location + toDelete.length(), source.length());
- }
- }
-
- private static String formatMeasureShort(Locale locale, NumberFormat numberFormatter,
- float value, MeasureUnit units) {
- final MeasureFormat measureFormatter = MeasureFormat.getInstance(
- locale, MeasureFormat.FormatWidth.SHORT, numberFormatter);
- return measureFormatter.format(new Measure(value, units));
- }
-
- private static final UnicodeSetSpanner SPACES_AND_CONTROLS =
- new UnicodeSetSpanner(new UnicodeSet("[[:Zs:][:Cf:]]").freeze());
-
- private static String formatRoundedBytesResult(
- @NonNull Context context, @NonNull RoundedBytesResult input) {
- final Locale locale = localeFromContext(context);
- final NumberFormat numberFormatter = getNumberFormatter(locale, input.fractionDigits);
- if (input.units == MeasureUnit.BYTE || input.units == PETABYTE) {
- // ICU spells out "byte" instead of "B", and can't format petabytes yet.
- final String formattedNumber = numberFormatter.format(input.value);
- return context.getString(com.android.internal.R.string.fileSizeSuffix,
- formattedNumber, getSuffixOverride(context.getResources(), input.units));
- } else {
- return formatMeasureShort(locale, numberFormatter, input.value, input.units);
- }
+ final BytesResult res = formatBytes(context.getResources(), sizeBytes, FLAG_SHORTER);
+ return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix,
+ res.value, res.units));
}
/** {@hide} */
public static BytesResult formatBytes(Resources res, long sizeBytes, int flags) {
- final RoundedBytesResult rounded = RoundedBytesResult.roundBytes(sizeBytes, flags);
- final Locale locale = res.getConfiguration().getLocales().get(0);
- final NumberFormat numberFormatter = getNumberFormatter(locale, rounded.fractionDigits);
- final String formattedNumber = numberFormatter.format(rounded.value);
- final String units;
- if (rounded.units == MeasureUnit.BYTE || rounded.units == PETABYTE) {
- // ICU spells out "byte" instead of "B", and can't format petabytes yet.
- units = getSuffixOverride(res, rounded.units);
- } else {
- // Since ICU does not give us access to the pattern, we need to extract the unit string
- // from ICU, which we do by taking out the formatted number out of the formatted string
- // and trimming the result of spaces and controls.
- final String formattedMeasure = formatMeasureShort(
- locale, numberFormatter, rounded.value, rounded.units);
- final String numberRemoved = deleteFirstFromString(formattedMeasure, formattedNumber);
- units = SPACES_AND_CONTROLS.trim(numberRemoved).toString();
+ final boolean isNegative = (sizeBytes < 0);
+ float result = isNegative ? -sizeBytes : sizeBytes;
+ int suffix = com.android.internal.R.string.byteShort;
+ long mult = 1;
+ if (result > 900) {
+ suffix = com.android.internal.R.string.kilobyteShort;
+ mult = 1000;
+ result = result / 1000;
}
- return new BytesResult(formattedNumber, units, rounded.roundedBytes);
- }
-
- /**
- * ICU doesn't support PETABYTE yet. Fake it so that we can treat all units the same way.
- */
- private static final MeasureUnit PETABYTE = createPetaByte();
-
- /**
- * Create a petabyte MeasureUnit without registering it with ICU.
- * ICU doesn't support user-create MeasureUnit and the only public (but hidden) method to do so
- * is {@link MeasureUnit#internalGetInstance(String, String)} which also registers the unit as
- * an available type and thus leaks it to code that doesn't expect or support it.
- * <p>This method uses reflection to create an instance of MeasureUnit to avoid leaking it. This
- * instance is <b>only</b> to be used in this class.
- */
- private static MeasureUnit createPetaByte() {
- try {
- Constructor<MeasureUnit> constructor = MeasureUnit.class
- .getDeclaredConstructor(String.class, String.class);
- constructor.setAccessible(true);
- return constructor.newInstance("digital", "petabyte");
- } catch (ReflectiveOperationException e) {
- throw new RuntimeException("Failed to create petabyte MeasureUnit", e);
+ if (result > 900) {
+ suffix = com.android.internal.R.string.megabyteShort;
+ mult *= 1000;
+ result = result / 1000;
}
- }
-
- private static class RoundedBytesResult {
- public final float value;
- public final MeasureUnit units;
- public final int fractionDigits;
- public final long roundedBytes;
-
- private RoundedBytesResult(
- float value, MeasureUnit units, int fractionDigits, long roundedBytes) {
- this.value = value;
- this.units = units;
- this.fractionDigits = fractionDigits;
- this.roundedBytes = roundedBytes;
+ if (result > 900) {
+ suffix = com.android.internal.R.string.gigabyteShort;
+ mult *= 1000;
+ result = result / 1000;
}
-
- /**
- * Returns a RoundedBytesResult object based on the input size in bytes and the rounding
- * flags. The result can be used for formatting.
- */
- static RoundedBytesResult roundBytes(long sizeBytes, int flags) {
- final boolean isNegative = (sizeBytes < 0);
- float result = isNegative ? -sizeBytes : sizeBytes;
- MeasureUnit units = MeasureUnit.BYTE;
- long mult = 1;
- if (result > 900) {
- units = MeasureUnit.KILOBYTE;
- mult = 1000;
- result = result / 1000;
- }
- if (result > 900) {
- units = MeasureUnit.MEGABYTE;
- mult *= 1000;
- result = result / 1000;
- }
- if (result > 900) {
- units = MeasureUnit.GIGABYTE;
- mult *= 1000;
- result = result / 1000;
- }
- if (result > 900) {
- units = MeasureUnit.TERABYTE;
- mult *= 1000;
- result = result / 1000;
- }
- if (result > 900) {
- units = PETABYTE;
- mult *= 1000;
- result = result / 1000;
- }
- // Note we calculate the rounded long by ourselves, but still let NumberFormat compute
- // the rounded value. NumberFormat.format(0.1) might not return "0.1" due to floating
- // point errors.
- final int roundFactor;
- final int roundDigits;
- if (mult == 1 || result >= 100) {
- roundFactor = 1;
- roundDigits = 0;
- } else if (result < 1) {
+ if (result > 900) {
+ suffix = com.android.internal.R.string.terabyteShort;
+ mult *= 1000;
+ result = result / 1000;
+ }
+ if (result > 900) {
+ suffix = com.android.internal.R.string.petabyteShort;
+ mult *= 1000;
+ result = result / 1000;
+ }
+ // Note we calculate the rounded long by ourselves, but still let String.format()
+ // compute the rounded value. String.format("%f", 0.1) might not return "0.1" due to
+ // floating point errors.
+ final int roundFactor;
+ final String roundFormat;
+ if (mult == 1 || result >= 100) {
+ roundFactor = 1;
+ roundFormat = "%.0f";
+ } else if (result < 1) {
+ roundFactor = 100;
+ roundFormat = "%.2f";
+ } else if (result < 10) {
+ if ((flags & FLAG_SHORTER) != 0) {
+ roundFactor = 10;
+ roundFormat = "%.1f";
+ } else {
roundFactor = 100;
- roundDigits = 2;
- } else if (result < 10) {
- if ((flags & FLAG_SHORTER) != 0) {
- roundFactor = 10;
- roundDigits = 1;
- } else {
- roundFactor = 100;
- roundDigits = 2;
- }
- } else { // 10 <= result < 100
- if ((flags & FLAG_SHORTER) != 0) {
- roundFactor = 1;
- roundDigits = 0;
- } else {
- roundFactor = 100;
- roundDigits = 2;
- }
+ roundFormat = "%.2f";
}
-
- if (isNegative) {
- result = -result;
+ } else { // 10 <= result < 100
+ if ((flags & FLAG_SHORTER) != 0) {
+ roundFactor = 1;
+ roundFormat = "%.0f";
+ } else {
+ roundFactor = 100;
+ roundFormat = "%.2f";
}
-
- // Note this might overflow if abs(result) >= Long.MAX_VALUE / 100, but that's like
- // 80PB so it's okay (for now)...
- final long roundedBytes =
- (flags & FLAG_CALCULATE_ROUNDED) == 0 ? 0
- : (((long) Math.round(result * roundFactor)) * mult / roundFactor);
-
- return new RoundedBytesResult(result, units, roundDigits, roundedBytes);
}
+
+ if (isNegative) {
+ result = -result;
+ }
+ final String roundedString = String.format(roundFormat, result);
+
+ // Note this might overflow if abs(result) >= Long.MAX_VALUE / 100, but that's like 80PB so
+ // it's okay (for now)...
+ final long roundedBytes =
+ (flags & FLAG_CALCULATE_ROUNDED) == 0 ? 0
+ : (((long) Math.round(result * roundFactor)) * mult / roundFactor);
+
+ final String units = res.getString(suffix);
+
+ return new BytesResult(roundedString, units, roundedBytes);
}
/**
diff --git a/android/text/format/Time.java b/android/text/format/Time.java
index bbd9c9c..562ae7a 100644
--- a/android/text/format/Time.java
+++ b/android/text/format/Time.java
@@ -358,7 +358,7 @@
}
/**
- * Return the current time in YYYYMMDDTHHMMSS<tz> format
+ * Return the current time in YYYYMMDDTHHMMSS<tz> format
*/
@Override
public String toString() {
@@ -738,6 +738,7 @@
* <p>
* You should also use <tt>toMillis(false)</tt> if you want
* to read back the same milliseconds that you set with {@link #set(long)}
+ * or {@link #set(Time)} or after parsing a date string.
*
* <p>
* This method can return {@code -1} when the date / time fields have been
@@ -745,8 +746,6 @@
* For example, when daylight savings transitions cause an hour to be
* skipped: times within that hour will return {@code -1} if isDst =
* {@code -1}.
- *
- * or {@link #set(Time)} or after parsing a date string.
*/
public long toMillis(boolean ignoreDst) {
calculator.copyFieldsFromTime(this);
diff --git a/android/text/style/AbsoluteSizeSpan.java b/android/text/style/AbsoluteSizeSpan.java
index 908ef55..3b4eea7 100644
--- a/android/text/style/AbsoluteSizeSpan.java
+++ b/android/text/style/AbsoluteSizeSpan.java
@@ -16,71 +16,105 @@
package android.text.style;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * A span that changes the size of the text it's attached to.
+ * <p>
+ * For example, the size of the text can be changed to 55dp like this:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with absolute size span");
+ *string.setSpan(new AbsoluteSizeSpan(55, true), 10, 23, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/absolutesizespan.png" />
+ * <figcaption>Text with text size updated.</figcaption>
+ */
public class AbsoluteSizeSpan extends MetricAffectingSpan implements ParcelableSpan {
private final int mSize;
- private boolean mDip;
+ private final boolean mDip;
/**
* Set the text size to <code>size</code> physical pixels.
*/
public AbsoluteSizeSpan(int size) {
- mSize = size;
+ this(size, false);
}
/**
- * Set the text size to <code>size</code> physical pixels,
- * or to <code>size</code> device-independent pixels if
- * <code>dip</code> is true.
+ * Set the text size to <code>size</code> physical pixels, or to <code>size</code>
+ * device-independent pixels if <code>dip</code> is true.
*/
public AbsoluteSizeSpan(int size, boolean dip) {
mSize = size;
mDip = dip;
}
- public AbsoluteSizeSpan(Parcel src) {
+ /**
+ * Creates an {@link AbsoluteSizeSpan} from a parcel.
+ */
+ public AbsoluteSizeSpan(@NonNull Parcel src) {
mSize = src.readInt();
mDip = src.readInt() != 0;
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.ABSOLUTE_SIZE_SPAN;
}
-
+
+ @Override
public int describeContents() {
return 0;
}
- public void writeToParcel(Parcel dest, int flags) {
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
- public void writeToParcelInternal(Parcel dest, int flags) {
+ @Override
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
dest.writeInt(mSize);
dest.writeInt(mDip ? 1 : 0);
}
+ /**
+ * Get the text size. This is in physical pixels if {@link #getDip()} returns false or in
+ * device-independent pixels if {@link #getDip()} returns true.
+ *
+ * @return the text size, either in physical pixels or device-independent pixels.
+ * @see AbsoluteSizeSpan#AbsoluteSizeSpan(int, boolean)
+ */
public int getSize() {
return mSize;
}
+ /**
+ * Returns whether the size is in device-independent pixels or not, depending on the
+ * <code>dip</code> flag passed in {@link #AbsoluteSizeSpan(int, boolean)}
+ *
+ * @return <code>true</code> if the size is in device-independent pixels, <code>false</code>
+ * otherwise
+ *
+ * @see #AbsoluteSizeSpan(int, boolean)
+ */
public boolean getDip() {
return mDip;
}
@Override
- public void updateDrawState(TextPaint ds) {
+ public void updateDrawState(@NonNull TextPaint ds) {
if (mDip) {
ds.setTextSize(mSize * ds.density);
} else {
@@ -89,7 +123,7 @@
}
@Override
- public void updateMeasureState(TextPaint ds) {
+ public void updateMeasureState(@NonNull TextPaint ds) {
if (mDip) {
ds.setTextSize(mSize * ds.density);
} else {
diff --git a/android/text/style/BackgroundColorSpan.java b/android/text/style/BackgroundColorSpan.java
index de05f50..44e3561 100644
--- a/android/text/style/BackgroundColorSpan.java
+++ b/android/text/style/BackgroundColorSpan.java
@@ -16,52 +16,88 @@
package android.text.style;
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * Changes the background color of the text to which the span is attached.
+ * <p>
+ * For example, to set a green background color for a text you would create a {@link
+ * android.text.SpannableString} based on the text and set the span.
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with a background color span");
+ *string.setSpan(new BackgroundColorSpan(color), 12, 28, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/backgroundcolorspan.png" />
+ * <figcaption>Set a background color for the text.</figcaption>
+ */
public class BackgroundColorSpan extends CharacterStyle
implements UpdateAppearance, ParcelableSpan {
private final int mColor;
- public BackgroundColorSpan(int color) {
+ /**
+ * Creates a {@link BackgroundColorSpan} from a color integer.
+ * <p>
+ *
+ * @param color color integer that defines the background color
+ * @see android.content.res.Resources#getColor(int, Resources.Theme)
+ */
+ public BackgroundColorSpan(@ColorInt int color) {
mColor = color;
}
- public BackgroundColorSpan(Parcel src) {
+ /**
+ * Creates a {@link BackgroundColorSpan} from a parcel.
+ */
+ public BackgroundColorSpan(@NonNull Parcel src) {
mColor = src.readInt();
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.BACKGROUND_COLOR_SPAN;
}
-
+
+ @Override
public int describeContents() {
return 0;
}
- public void writeToParcel(Parcel dest, int flags) {
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
- public void writeToParcelInternal(Parcel dest, int flags) {
+ @Override
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
dest.writeInt(mColor);
}
+ /**
+ * @return the background color of this span.
+ * @see BackgroundColorSpan#BackgroundColorSpan(int)
+ */
+ @ColorInt
public int getBackgroundColor() {
return mColor;
}
+ /**
+ * Updates the background color of the TextPaint.
+ */
@Override
- public void updateDrawState(TextPaint ds) {
- ds.bgColor = mColor;
+ public void updateDrawState(@NonNull TextPaint textPaint) {
+ textPaint.bgColor = mColor;
}
}
diff --git a/android/text/style/BulletSpan.java b/android/text/style/BulletSpan.java
index 43dd0ff..70175c8 100644
--- a/android/text/style/BulletSpan.java
+++ b/android/text/style/BulletSpan.java
@@ -16,6 +16,11 @@
package android.text.style;
+import android.annotation.ColorInt;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Px;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
@@ -26,38 +31,108 @@
import android.text.Spanned;
import android.text.TextUtils;
+/**
+ * A span which styles paragraphs as bullet points (respecting layout direction).
+ * <p>
+ * BulletSpans must be attached from the first character to the last character of a single
+ * paragraph, otherwise the bullet point will not be displayed but the first paragraph encountered
+ * will have a leading margin.
+ * <p>
+ * BulletSpans allow configuring the following elements:
+ * <ul>
+ * <li><b>gap width</b> - the distance, in pixels, between the bullet point and the paragraph.
+ * Default value is 2px.</li>
+ * <li><b>color</b> - the bullet point color. By default, the bullet point color is 0 - no color,
+ * so it uses the TextView's text color.</li>
+ * <li><b>bullet radius</b> - the radius, in pixels, of the bullet point. Default value is
+ * 4px.</li>
+ * </ul>
+ * For example, a BulletSpan using the default values can be constructed like this:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with\nBullet point");
+ *string.setSpan(new BulletSpan(), 10, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/defaultbulletspan.png" />
+ * <figcaption>BulletSpan constructed with default values.</figcaption>
+ * <p>
+ * <p>
+ * To construct a BulletSpan with a gap width of 40px, green bullet point and bullet radius of
+ * 20px:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with\nBullet point");
+ *string.setSpan(new BulletSpan(40, color, 20), 10, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/custombulletspan.png" />
+ * <figcaption>Customized BulletSpan.</figcaption>
+ */
public class BulletSpan implements LeadingMarginSpan, ParcelableSpan {
- private final int mGapWidth;
- private final boolean mWantColor;
- private final int mColor;
-
// Bullet is slightly bigger to avoid aliasing artifacts on mdpi devices.
- private static final float BULLET_RADIUS = 3 * 1.2f;
- private static Path sBulletPath = null;
+ private static final int STANDARD_BULLET_RADIUS = 4;
public static final int STANDARD_GAP_WIDTH = 2;
+ private static final int STANDARD_COLOR = 0;
+ @Px
+ private final int mGapWidth;
+ @Px
+ private final int mBulletRadius;
+ private Path mBulletPath = null;
+ @ColorInt
+ private final int mColor;
+ private final boolean mWantColor;
+
+ /**
+ * Creates a {@link BulletSpan} with the default values.
+ */
public BulletSpan() {
- mGapWidth = STANDARD_GAP_WIDTH;
- mWantColor = false;
- mColor = 0;
+ this(STANDARD_GAP_WIDTH, STANDARD_COLOR, false, STANDARD_BULLET_RADIUS);
}
+ /**
+ * Creates a {@link BulletSpan} based on a gap width
+ *
+ * @param gapWidth the distance, in pixels, between the bullet point and the paragraph.
+ */
public BulletSpan(int gapWidth) {
- mGapWidth = gapWidth;
- mWantColor = false;
- mColor = 0;
+ this(gapWidth, STANDARD_COLOR, false, STANDARD_BULLET_RADIUS);
}
- public BulletSpan(int gapWidth, int color) {
+ /**
+ * Creates a {@link BulletSpan} based on a gap width and a color integer.
+ *
+ * @param gapWidth the distance, in pixels, between the bullet point and the paragraph.
+ * @param color the bullet point color, as a color integer
+ * @see android.content.res.Resources#getColor(int, Resources.Theme)
+ */
+ public BulletSpan(int gapWidth, @ColorInt int color) {
+ this(gapWidth, color, true, STANDARD_BULLET_RADIUS);
+ }
+
+ /**
+ * Creates a {@link BulletSpan} based on a gap width and a color integer.
+ *
+ * @param gapWidth the distance, in pixels, between the bullet point and the paragraph.
+ * @param color the bullet point color, as a color integer.
+ * @param bulletRadius the radius of the bullet point, in pixels.
+ * @see android.content.res.Resources#getColor(int, Resources.Theme)
+ */
+ public BulletSpan(int gapWidth, @ColorInt int color, @IntRange(from = 0) int bulletRadius) {
+ this(gapWidth, color, true, bulletRadius);
+ }
+
+ private BulletSpan(int gapWidth, @ColorInt int color, boolean wantColor,
+ @IntRange(from = 0) int bulletRadius) {
mGapWidth = gapWidth;
- mWantColor = true;
+ mBulletRadius = bulletRadius;
mColor = color;
+ mWantColor = wantColor;
}
- public BulletSpan(Parcel src) {
+ /**
+ * Creates a {@link BulletSpan} from a parcel.
+ */
+ public BulletSpan(@NonNull Parcel src) {
mGapWidth = src.readInt();
mWantColor = src.readInt() != 0;
mColor = src.readInt();
+ mBulletRadius = src.readInt();
}
@Override
@@ -77,68 +152,97 @@
}
@Override
- public void writeToParcel(Parcel dest, int flags) {
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
@Override
- public void writeToParcelInternal(Parcel dest, int flags) {
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
dest.writeInt(mGapWidth);
dest.writeInt(mWantColor ? 1 : 0);
dest.writeInt(mColor);
+ dest.writeInt(mBulletRadius);
}
@Override
public int getLeadingMargin(boolean first) {
- return (int) (2 * BULLET_RADIUS + mGapWidth);
+ return 2 * mBulletRadius + mGapWidth;
+ }
+
+ /**
+ * Get the distance, in pixels, between the bullet point and the paragraph.
+ *
+ * @return the distance, in pixels, between the bullet point and the paragraph.
+ */
+ public int getGapWidth() {
+ return mGapWidth;
+ }
+
+ /**
+ * Get the radius, in pixels, of the bullet point.
+ *
+ * @return the radius, in pixels, of the bullet point.
+ */
+ public int getBulletRadius() {
+ return mBulletRadius;
+ }
+
+ /**
+ * Get the bullet point color.
+ *
+ * @return the bullet point color
+ */
+ public int getColor() {
+ return mColor;
}
@Override
- public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
- int top, int baseline, int bottom,
- CharSequence text, int start, int end,
- boolean first, Layout l) {
+ public void drawLeadingMargin(@NonNull Canvas canvas, @NonNull Paint paint, int x, int dir,
+ int top, int baseline, int bottom,
+ @NonNull CharSequence text, int start, int end,
+ boolean first, @Nullable Layout layout) {
if (((Spanned) text).getSpanStart(this) == start) {
- Paint.Style style = p.getStyle();
+ Paint.Style style = paint.getStyle();
int oldcolor = 0;
if (mWantColor) {
- oldcolor = p.getColor();
- p.setColor(mColor);
+ oldcolor = paint.getColor();
+ paint.setColor(mColor);
}
- p.setStyle(Paint.Style.FILL);
+ paint.setStyle(Paint.Style.FILL);
- if (l != null) {
+ if (layout != null) {
// "bottom" position might include extra space as a result of line spacing
// configuration. Subtract extra space in order to show bullet in the vertical
// center of characters.
- final int line = l.getLineForOffset(start);
- bottom = bottom - l.getLineExtra(line);
+ final int line = layout.getLineForOffset(start);
+ bottom = bottom - layout.getLineExtra(line);
}
- final float y = (top + bottom) / 2f;
+ final float yPosition = (top + bottom) / 2f;
+ final float xPosition = x + dir * mBulletRadius;
- if (c.isHardwareAccelerated()) {
- if (sBulletPath == null) {
- sBulletPath = new Path();
- sBulletPath.addCircle(0.0f, 0.0f, BULLET_RADIUS, Direction.CW);
+ if (canvas.isHardwareAccelerated()) {
+ if (mBulletPath == null) {
+ mBulletPath = new Path();
+ mBulletPath.addCircle(0.0f, 0.0f, mBulletRadius, Direction.CW);
}
- c.save();
- c.translate(x + dir * BULLET_RADIUS, y);
- c.drawPath(sBulletPath, p);
- c.restore();
+ canvas.save();
+ canvas.translate(xPosition, yPosition);
+ canvas.drawPath(mBulletPath, paint);
+ canvas.restore();
} else {
- c.drawCircle(x + dir * BULLET_RADIUS, y, BULLET_RADIUS, p);
+ canvas.drawCircle(xPosition, yPosition, mBulletRadius, paint);
}
if (mWantColor) {
- p.setColor(oldcolor);
+ paint.setColor(oldcolor);
}
- p.setStyle(style);
+ paint.setStyle(style);
}
}
}
diff --git a/android/text/style/ForegroundColorSpan.java b/android/text/style/ForegroundColorSpan.java
index 2bc6d54..f770674 100644
--- a/android/text/style/ForegroundColorSpan.java
+++ b/android/text/style/ForegroundColorSpan.java
@@ -17,53 +17,88 @@
package android.text.style;
import android.annotation.ColorInt;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * Changes the color of the text to which the span is attached.
+ * <p>
+ * For example, to set a green text color you would create a {@link
+ * android.text.SpannableString} based on the text and set the span.
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with a foreground color span");
+ *string.setSpan(new ForegroundColorSpan(color), 12, 28, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/foregroundcolorspan.png" />
+ * <figcaption>Set a text color.</figcaption>
+ */
public class ForegroundColorSpan extends CharacterStyle
implements UpdateAppearance, ParcelableSpan {
private final int mColor;
+ /**
+ * Creates a {@link ForegroundColorSpan} from a color integer.
+ * <p>
+ * To get the color integer associated with a particular color resource ID, use
+ * {@link android.content.res.Resources#getColor(int, Resources.Theme)}
+ *
+ * @param color color integer that defines the text color
+ */
public ForegroundColorSpan(@ColorInt int color) {
mColor = color;
}
- public ForegroundColorSpan(Parcel src) {
+ /**
+ * Creates a {@link ForegroundColorSpan} from a parcel.
+ */
+ public ForegroundColorSpan(@NonNull Parcel src) {
mColor = src.readInt();
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.FOREGROUND_COLOR_SPAN;
}
+ @Override
public int describeContents() {
return 0;
}
- public void writeToParcel(Parcel dest, int flags) {
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
- public void writeToParcelInternal(Parcel dest, int flags) {
+ @Override
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
dest.writeInt(mColor);
}
+ /**
+ * @return the foreground color of this span.
+ * @see ForegroundColorSpan#ForegroundColorSpan(int)
+ */
@ColorInt
public int getForegroundColor() {
return mColor;
}
+ /**
+ * Updates the color of the TextPaint to the foreground color.
+ */
@Override
- public void updateDrawState(TextPaint ds) {
- ds.setColor(mColor);
+ public void updateDrawState(@NonNull TextPaint textPaint) {
+ textPaint.setColor(mColor);
}
}
diff --git a/android/text/style/RelativeSizeSpan.java b/android/text/style/RelativeSizeSpan.java
index 95f048a..3094f27 100644
--- a/android/text/style/RelativeSizeSpan.java
+++ b/android/text/style/RelativeSizeSpan.java
@@ -16,56 +16,85 @@
package android.text.style;
+import android.annotation.FloatRange;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * Uniformly scales the size of the text to which it's attached by a certain proportion.
+ * <p>
+ * For example, a <code>RelativeSizeSpan</code> that increases the text size by 50% can be
+ * constructed like this:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with relative size span");
+ *string.setSpan(new RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/relativesizespan.png" />
+ * <figcaption>Text increased by 50% with <code>RelativeSizeSpan</code>.</figcaption>
+ */
public class RelativeSizeSpan extends MetricAffectingSpan implements ParcelableSpan {
private final float mProportion;
- public RelativeSizeSpan(float proportion) {
+ /**
+ * Creates a {@link RelativeSizeSpan} based on a proportion.
+ *
+ * @param proportion the proportion with which the text is scaled.
+ */
+ public RelativeSizeSpan(@FloatRange(from = 0) float proportion) {
mProportion = proportion;
}
- public RelativeSizeSpan(Parcel src) {
+ /**
+ * Creates a {@link RelativeSizeSpan} from a parcel.
+ */
+ public RelativeSizeSpan(@NonNull Parcel src) {
mProportion = src.readFloat();
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.RELATIVE_SIZE_SPAN;
}
-
+
+ @Override
public int describeContents() {
return 0;
}
- public void writeToParcel(Parcel dest, int flags) {
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
- public void writeToParcelInternal(Parcel dest, int flags) {
+ @Override
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
dest.writeFloat(mProportion);
}
+ /**
+ * @return the proportion with which the text size is changed.
+ */
public float getSizeChange() {
return mProportion;
}
@Override
- public void updateDrawState(TextPaint ds) {
+ public void updateDrawState(@NonNull TextPaint ds) {
ds.setTextSize(ds.getTextSize() * mProportion);
}
@Override
- public void updateMeasureState(TextPaint ds) {
+ public void updateMeasureState(@NonNull TextPaint ds) {
ds.setTextSize(ds.getTextSize() * mProportion);
}
}
diff --git a/android/text/style/ScaleXSpan.java b/android/text/style/ScaleXSpan.java
index d085018..6ef4cec 100644
--- a/android/text/style/ScaleXSpan.java
+++ b/android/text/style/ScaleXSpan.java
@@ -16,45 +16,79 @@
package android.text.style;
+import android.annotation.FloatRange;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * Scales horizontally the size of the text to which it's attached by a certain factor.
+ * <p>
+ * Values > 1.0 will stretch the text wider. Values < 1.0 will stretch the text narrower.
+ * <p>
+ * For example, a <code>ScaleXSpan</code> that stretches the text size by 100% can be
+ * constructed like this:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with ScaleX span");
+ *string.setSpan(new ScaleXSpan(2f), 10, 16, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/scalexspan.png" />
+ * <figcaption>Text scaled by 100% with <code>ScaleXSpan</code>.</figcaption>
+ */
public class ScaleXSpan extends MetricAffectingSpan implements ParcelableSpan {
private final float mProportion;
- public ScaleXSpan(float proportion) {
+ /**
+ * Creates a {@link ScaleXSpan} based on a proportion. Values > 1.0 will stretch the text wider.
+ * Values < 1.0 will stretch the text narrower.
+ *
+ * @param proportion the horizontal scale factor.
+ */
+ public ScaleXSpan(@FloatRange(from = 0) float proportion) {
mProportion = proportion;
}
- public ScaleXSpan(Parcel src) {
+ /**
+ * Creates a {@link ScaleXSpan} from a parcel.
+ */
+ public ScaleXSpan(@NonNull Parcel src) {
mProportion = src.readFloat();
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.SCALE_X_SPAN;
}
-
+
+ @Override
public int describeContents() {
return 0;
}
- public void writeToParcel(Parcel dest, int flags) {
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
- public void writeToParcelInternal(Parcel dest, int flags) {
+ @Override
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
dest.writeFloat(mProportion);
}
+ /**
+ * Get the horizontal scale factor for the text.
+ *
+ * @return the horizontal scale factor.
+ */
public float getScaleX() {
return mProportion;
}
diff --git a/android/text/style/StrikethroughSpan.java b/android/text/style/StrikethroughSpan.java
index 1389704..a630505 100644
--- a/android/text/style/StrikethroughSpan.java
+++ b/android/text/style/StrikethroughSpan.java
@@ -16,42 +16,65 @@
package android.text.style;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * A span that strikes through the text it's attached to.
+ * <p>
+ * The span can be used like this:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with strikethrough span");
+ *string.setSpan(new StrikethroughSpan(), 10, 23, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/strikethroughspan.png" />
+ * <figcaption>Strikethrough text.</figcaption>
+ */
public class StrikethroughSpan extends CharacterStyle
implements UpdateAppearance, ParcelableSpan {
+
+ /**
+ * Creates a {@link StrikethroughSpan}.
+ */
public StrikethroughSpan() {
}
-
- public StrikethroughSpan(Parcel src) {
+
+ /**
+ * Creates a {@link StrikethroughSpan} from a parcel.
+ */
+ public StrikethroughSpan(@NonNull Parcel src) {
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.STRIKETHROUGH_SPAN;
}
-
+
+ @Override
public int describeContents() {
return 0;
}
- public void writeToParcel(Parcel dest, int flags) {
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
- public void writeToParcelInternal(Parcel dest, int flags) {
+ @Override
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
}
@Override
- public void updateDrawState(TextPaint ds) {
+ public void updateDrawState(@NonNull TextPaint ds) {
ds.setStrikeThruText(true);
}
}
diff --git a/android/text/style/SubscriptSpan.java b/android/text/style/SubscriptSpan.java
index f1b0d38..3d15aad 100644
--- a/android/text/style/SubscriptSpan.java
+++ b/android/text/style/SubscriptSpan.java
@@ -16,46 +16,74 @@
package android.text.style;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * The span that moves the position of the text baseline lower.
+ * <p>
+ * The span can be used like this:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("☕- C8H10N4O2\n");
+ *string.setSpan(new SubscriptSpan(), 4, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ *string.setSpan(new SubscriptSpan(), 6, 8, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ *string.setSpan(new SubscriptSpan(), 9, 10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ *string.setSpan(new SubscriptSpan(), 11, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/subscriptspan.png" />
+ * <figcaption>Text with <code>SubscriptSpan</code>.</figcaption>
+ * Note: Since the span affects the position of the text, if the text is on the last line of a
+ * TextView, it may appear cut.
+ */
public class SubscriptSpan extends MetricAffectingSpan implements ParcelableSpan {
+
+ /**
+ * Creates a {@link SubscriptSpan}.
+ */
public SubscriptSpan() {
}
-
- public SubscriptSpan(Parcel src) {
+
+ /**
+ * Creates a {@link SubscriptSpan} from a parcel.
+ */
+ public SubscriptSpan(@NonNull Parcel src) {
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.SUBSCRIPT_SPAN;
}
-
+
+ @Override
public int describeContents() {
return 0;
}
+ @Override
public void writeToParcel(Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
+ @Override
public void writeToParcelInternal(Parcel dest, int flags) {
}
@Override
- public void updateDrawState(TextPaint tp) {
- tp.baselineShift -= (int) (tp.ascent() / 2);
+ public void updateDrawState(@NonNull TextPaint textPaint) {
+ textPaint.baselineShift -= (int) (textPaint.ascent() / 2);
}
@Override
- public void updateMeasureState(TextPaint tp) {
- tp.baselineShift -= (int) (tp.ascent() / 2);
+ public void updateMeasureState(@NonNull TextPaint textPaint) {
+ textPaint.baselineShift -= (int) (textPaint.ascent() / 2);
}
}
diff --git a/android/text/style/SuperscriptSpan.java b/android/text/style/SuperscriptSpan.java
index abcf688..3dc9d3f 100644
--- a/android/text/style/SuperscriptSpan.java
+++ b/android/text/style/SuperscriptSpan.java
@@ -16,46 +16,71 @@
package android.text.style;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * The span that moves the position of the text baseline higher.
+ * <p>
+ * The span can be used like this:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("1st example");
+ *string.setSpan(new SuperscriptSpan(), 1, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/superscriptspan.png" />
+ * <figcaption>Text with <code>SuperscriptSpan</code>.</figcaption>
+ * Note: Since the span affects the position of the text, if the text is on the first line of a
+ * TextView, it may appear cut. This can be avoided by decreasing the text size with an {@link
+ * AbsoluteSizeSpan}
+ */
public class SuperscriptSpan extends MetricAffectingSpan implements ParcelableSpan {
+ /**
+ * Creates a {@link SuperscriptSpan}.
+ */
public SuperscriptSpan() {
}
-
- public SuperscriptSpan(Parcel src) {
+
+ /**
+ * Creates a {@link SuperscriptSpan} from a parcel.
+ */
+ public SuperscriptSpan(@NonNull Parcel src) {
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.SUPERSCRIPT_SPAN;
}
-
+
+ @Override
public int describeContents() {
return 0;
}
- public void writeToParcel(Parcel dest, int flags) {
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
- public void writeToParcelInternal(Parcel dest, int flags) {
+ @Override
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
}
@Override
- public void updateDrawState(TextPaint tp) {
- tp.baselineShift += (int) (tp.ascent() / 2);
+ public void updateDrawState(@NonNull TextPaint textPaint) {
+ textPaint.baselineShift += (int) (textPaint.ascent() / 2);
}
@Override
- public void updateMeasureState(TextPaint tp) {
- tp.baselineShift += (int) (tp.ascent() / 2);
+ public void updateMeasureState(@NonNull TextPaint textPaint) {
+ textPaint.baselineShift += (int) (textPaint.ascent() / 2);
}
}
diff --git a/android/text/style/UnderlineSpan.java b/android/text/style/UnderlineSpan.java
index 9024dcd..800838e 100644
--- a/android/text/style/UnderlineSpan.java
+++ b/android/text/style/UnderlineSpan.java
@@ -16,42 +16,65 @@
package android.text.style;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * A span that underlines the text it's attached to.
+ * <p>
+ * The span can be used like this:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with underline span");
+ *string.setSpan(new UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/underlinespan.png" />
+ * <figcaption>Underlined text.</figcaption>
+ */
public class UnderlineSpan extends CharacterStyle
implements UpdateAppearance, ParcelableSpan {
+
+ /**
+ * Creates an {@link UnderlineSpan}.
+ */
public UnderlineSpan() {
}
-
- public UnderlineSpan(Parcel src) {
+
+ /**
+ * Creates an {@link UnderlineSpan} from a parcel.
+ */
+ public UnderlineSpan(@NonNull Parcel src) {
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.UNDERLINE_SPAN;
}
-
+
+ @Override
public int describeContents() {
return 0;
}
- public void writeToParcel(Parcel dest, int flags) {
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
- public void writeToParcelInternal(Parcel dest, int flags) {
+ @Override
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
}
@Override
- public void updateDrawState(TextPaint ds) {
+ public void updateDrawState(@NonNull TextPaint ds) {
ds.setUnderlineText(true);
}
}
diff --git a/android/util/DataUnit.java b/android/util/DataUnit.java
new file mode 100644
index 0000000..ea4266e
--- /dev/null
+++ b/android/util/DataUnit.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2007 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.util;
+
+import java.time.temporal.ChronoUnit;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A {@code DataUnit} represents data sizes at a given unit of granularity and
+ * provides utility methods to convert across units.
+ * <p>
+ * Note that both SI units (powers of 10) and IEC units (powers of 2) are
+ * supported, and you'll need to pick the correct one for your use-case. For
+ * example, Wikipedia defines a "kilobyte" as an SI unit of 1000 bytes, and a
+ * "kibibyte" as an IEC unit of 1024 bytes.
+ * <p>
+ * This design is mirrored after {@link TimeUnit} and {@link ChronoUnit}.
+ */
+public enum DataUnit {
+ KILOBYTES { @Override public long toBytes(long v) { return v * 1_000; } },
+ MEGABYTES { @Override public long toBytes(long v) { return v * 1_000_000; } },
+ GIGABYTES { @Override public long toBytes(long v) { return v * 1_000_000_000; } },
+ KIBIBYTES { @Override public long toBytes(long v) { return v * 1_024; } },
+ MEBIBYTES { @Override public long toBytes(long v) { return v * 1_048_576; } },
+ GIBIBYTES { @Override public long toBytes(long v) { return v * 1_073_741_824; } };
+
+ public long toBytes(long v) {
+ throw new AbstractMethodError();
+ }
+}
diff --git a/android/util/FeatureFlagUtils.java b/android/util/FeatureFlagUtils.java
index bfb5130..25a177e 100644
--- a/android/util/FeatureFlagUtils.java
+++ b/android/util/FeatureFlagUtils.java
@@ -38,13 +38,15 @@
static {
DEFAULT_FLAGS = new HashMap<>();
DEFAULT_FLAGS.put("device_info_v2", "true");
- DEFAULT_FLAGS.put("new_settings_suggestion", "true");
- DEFAULT_FLAGS.put("settings_search_v2", "true");
- DEFAULT_FLAGS.put("settings_app_info_v2", "false");
+ DEFAULT_FLAGS.put("settings_app_info_v2", "true");
DEFAULT_FLAGS.put("settings_connected_device_v2", "true");
- DEFAULT_FLAGS.put("settings_battery_v2", "false");
+ DEFAULT_FLAGS.put("settings_battery_v2", "true");
DEFAULT_FLAGS.put("settings_battery_display_app_list", "false");
- DEFAULT_FLAGS.put("settings_security_settings_v2", "false");
+ DEFAULT_FLAGS.put("settings_security_settings_v2", "true");
+ DEFAULT_FLAGS.put("settings_zone_picker_v2", "true");
+ DEFAULT_FLAGS.put("settings_suggestion_ui_v2", "false");
+ DEFAULT_FLAGS.put("settings_about_phone_v2", "false");
+ DEFAULT_FLAGS.put("settings_bluetooth_while_driving", "false");
}
/**
diff --git a/android/util/KeyValueListParser.java b/android/util/KeyValueListParser.java
index 0a00794..7eef63e 100644
--- a/android/util/KeyValueListParser.java
+++ b/android/util/KeyValueListParser.java
@@ -17,6 +17,9 @@
import android.text.TextUtils;
+import java.time.Duration;
+import java.time.format.DateTimeParseException;
+
/**
* Parses a list of key=value pairs, separated by some delimiter, and puts the results in
* an internal Map. Values can be then queried by key, or if not found, a default value
@@ -189,4 +192,24 @@
public String keyAt(int index) {
return mValues.keyAt(index);
}
+
+ /**
+ * {@hide}
+ * Parse a duration in millis based on java.time.Duration or just a number (millis)
+ */
+ public long getDurationMillis(String key, long def) {
+ String value = mValues.get(key);
+ if (value != null) {
+ try {
+ if (value.startsWith("P") || value.startsWith("p")) {
+ return Duration.parse(value).toMillis();
+ } else {
+ return Long.parseLong(value);
+ }
+ } catch (NumberFormatException | DateTimeParseException e) {
+ // fallthrough
+ }
+ }
+ return def;
+ }
}
diff --git a/android/util/MutableBoolean.java b/android/util/MutableBoolean.java
index ed837ab..44e73cc 100644
--- a/android/util/MutableBoolean.java
+++ b/android/util/MutableBoolean.java
@@ -17,7 +17,9 @@
package android.util;
/**
+ * @deprecated This class will be removed from a future version of the Android API.
*/
+@Deprecated
public final class MutableBoolean {
public boolean value;
diff --git a/android/util/MutableByte.java b/android/util/MutableByte.java
index cc6b00a..b9ec25d 100644
--- a/android/util/MutableByte.java
+++ b/android/util/MutableByte.java
@@ -17,7 +17,9 @@
package android.util;
/**
+ * @deprecated This class will be removed from a future version of the Android API.
*/
+@Deprecated
public final class MutableByte {
public byte value;
diff --git a/android/util/MutableChar.java b/android/util/MutableChar.java
index 9a2e2bc..9f7a9ae 100644
--- a/android/util/MutableChar.java
+++ b/android/util/MutableChar.java
@@ -17,7 +17,9 @@
package android.util;
/**
+ * @deprecated This class will be removed from a future version of the Android API.
*/
+@Deprecated
public final class MutableChar {
public char value;
diff --git a/android/util/MutableDouble.java b/android/util/MutableDouble.java
index bd7329a..56e539b 100644
--- a/android/util/MutableDouble.java
+++ b/android/util/MutableDouble.java
@@ -17,7 +17,9 @@
package android.util;
/**
+ * @deprecated This class will be removed from a future version of the Android API.
*/
+@Deprecated
public final class MutableDouble {
public double value;
diff --git a/android/util/MutableFloat.java b/android/util/MutableFloat.java
index e6f2d7d..6d7ad59 100644
--- a/android/util/MutableFloat.java
+++ b/android/util/MutableFloat.java
@@ -17,7 +17,9 @@
package android.util;
/**
+ * @deprecated This class will be removed from a future version of the Android API.
*/
+@Deprecated
public final class MutableFloat {
public float value;
diff --git a/android/util/MutableInt.java b/android/util/MutableInt.java
index a3d8606..bb24566 100644
--- a/android/util/MutableInt.java
+++ b/android/util/MutableInt.java
@@ -17,7 +17,9 @@
package android.util;
/**
+ * @deprecated This class will be removed from a future version of the Android API.
*/
+@Deprecated
public final class MutableInt {
public int value;
diff --git a/android/util/MutableLong.java b/android/util/MutableLong.java
index 575068e..86e70e1 100644
--- a/android/util/MutableLong.java
+++ b/android/util/MutableLong.java
@@ -17,7 +17,9 @@
package android.util;
/**
+ * @deprecated This class will be removed from a future version of the Android API.
*/
+@Deprecated
public final class MutableLong {
public long value;
diff --git a/android/util/MutableShort.java b/android/util/MutableShort.java
index 48fb232..b94ab07 100644
--- a/android/util/MutableShort.java
+++ b/android/util/MutableShort.java
@@ -17,7 +17,9 @@
package android.util;
/**
+ * @deprecated This class will be removed from a future version of the Android API.
*/
+@Deprecated
public final class MutableShort {
public short value;
diff --git a/android/util/PackageUtils.java b/android/util/PackageUtils.java
index e2e9d53..a5e3818 100644
--- a/android/util/PackageUtils.java
+++ b/android/util/PackageUtils.java
@@ -105,7 +105,7 @@
* @param data The data.
* @return The digest or null if an error occurs.
*/
- public static @Nullable String computeSha256Digest(@NonNull byte[] data) {
+ public static @Nullable byte[] computeSha256DigestBytes(@NonNull byte[] data) {
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance("SHA256");
@@ -116,6 +116,15 @@
messageDigest.update(data);
- return ByteStringUtils.toHexString(messageDigest.digest());
+ return messageDigest.digest();
+ }
+
+ /**
+ * Computes the SHA256 digest of some data.
+ * @param data The data.
+ * @return The digest or null if an error occurs.
+ */
+ public static @Nullable String computeSha256Digest(@NonNull byte[] data) {
+ return ByteStringUtils.toHexString(computeSha256DigestBytes(data));
}
}
diff --git a/android/util/StatsManager.java b/android/util/StatsManager.java
index 26a3c36..687aa83 100644
--- a/android/util/StatsManager.java
+++ b/android/util/StatsManager.java
@@ -17,19 +17,33 @@
import android.Manifest;
import android.annotation.RequiresPermission;
-import android.annotation.SystemApi;
import android.os.IBinder;
import android.os.IStatsManager;
import android.os.RemoteException;
import android.os.ServiceManager;
+
+/*
+ *
+ *
+ *
+ *
+ * THIS ENTIRE FILE IS ONLY TEMPORARY TO PREVENT BREAKAGES OF DEPENDENCIES ON OLD APIS.
+ * The new StatsManager is to be found in android.app.StatsManager.
+ * TODO: Delete this file!
+ *
+ *
+ *
+ *
+ */
+
+
/**
* API for StatsD clients to send configurations and retrieve data.
*
* @hide
*/
-@SystemApi
-public final class StatsManager {
+public class StatsManager {
IStatsManager mService;
private static final String TAG = "StatsManager";
@@ -42,10 +56,20 @@
}
/**
+ * Temporary to prevent build failures. Will be deleted.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public boolean addConfiguration(String configKey, byte[] config, String pkg, String cls) {
+ // To prevent breakages of dependencies on old API.
+
+ return false;
+ }
+
+ /**
* Clients can send a configuration and simultaneously registers the name of a broadcast
* receiver that listens for when it should request data.
*
- * @param configKey An arbitrary string that allows clients to track the configuration.
+ * @param configKey An arbitrary integer that allows clients to track the configuration.
* @param config Wire-encoded StatsDConfig proto that specifies metrics (and all
* dependencies eg, conditions and matchers).
* @param pkg The package name to receive the broadcast.
@@ -53,7 +77,7 @@
* @return true if successful
*/
@RequiresPermission(Manifest.permission.DUMP)
- public boolean addConfiguration(String configKey, byte[] config, String pkg, String cls) {
+ public boolean addConfiguration(long configKey, byte[] config, String pkg, String cls) {
synchronized (this) {
try {
IStatsManager service = getIStatsManagerLocked();
@@ -70,13 +94,22 @@
}
/**
+ * Temporary to prevent build failures. Will be deleted.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public boolean removeConfiguration(String configKey) {
+ // To prevent breakages of old dependencies.
+ return false;
+ }
+
+ /**
* Remove a configuration from logging.
*
* @param configKey Configuration key to remove.
* @return true if successful
*/
@RequiresPermission(Manifest.permission.DUMP)
- public boolean removeConfiguration(String configKey) {
+ public boolean removeConfiguration(long configKey) {
synchronized (this) {
try {
IStatsManager service = getIStatsManagerLocked();
@@ -93,6 +126,16 @@
}
/**
+ * Temporary to prevent build failures. Will be deleted.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public byte[] getData(String configKey) {
+ // TODO: remove this and all other methods with String-based config keys.
+ // To prevent build breakages of dependencies.
+ return null;
+ }
+
+ /**
* Clients can request data with a binder call. This getter is destructive and also clears
* the retrieved metrics from statsd memory.
*
@@ -100,7 +143,7 @@
* @return Serialized ConfigMetricsReportList proto. Returns null on failure.
*/
@RequiresPermission(Manifest.permission.DUMP)
- public byte[] getData(String configKey) {
+ public byte[] getData(long configKey) {
synchronized (this) {
try {
IStatsManager service = getIStatsManagerLocked();
diff --git a/android/util/TimeUtils.java b/android/util/TimeUtils.java
index cc4a0b6..84ae20b 100644
--- a/android/util/TimeUtils.java
+++ b/android/util/TimeUtils.java
@@ -18,30 +18,18 @@
import android.os.SystemClock;
-import java.io.PrintWriter;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
import libcore.util.TimeZoneFinder;
import libcore.util.ZoneInfoDB;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
/**
* A class containing utility methods related to time zones.
*/
public class TimeUtils {
/** @hide */ public TimeUtils() {}
- private static final boolean DBG = false;
- private static final String TAG = "TimeUtils";
-
- /** Cached results of getTimeZonesWithUniqueOffsets */
- private static final Object sLastUniqueLockObj = new Object();
- private static List<String> sLastUniqueZoneOffsets = null;
- private static String sLastUniqueCountry = null;
-
/** {@hide} */
private static SimpleDateFormat sLoggingFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@@ -76,86 +64,6 @@
}
/**
- * Returns an immutable list of unique time zone IDs for the country.
- *
- * @param country to find
- * @return unmodifiable list of unique time zones, maybe empty but never null.
- * @hide
- */
- public static List<String> getTimeZoneIdsWithUniqueOffsets(String country) {
- synchronized(sLastUniqueLockObj) {
- if ((country != null) && country.equals(sLastUniqueCountry)) {
- if (DBG) {
- Log.d(TAG, "getTimeZonesWithUniqueOffsets(" +
- country + "): return cached version");
- }
- return sLastUniqueZoneOffsets;
- }
- }
-
- Collection<android.icu.util.TimeZone> zones = getIcuTimeZones(country);
- ArrayList<android.icu.util.TimeZone> uniqueTimeZones = new ArrayList<>();
- for (android.icu.util.TimeZone zone : zones) {
- // See if we already have this offset,
- // Using slow but space efficient and these are small.
- boolean found = false;
- for (int i = 0; i < uniqueTimeZones.size(); i++) {
- if (uniqueTimeZones.get(i).getRawOffset() == zone.getRawOffset()) {
- found = true;
- break;
- }
- }
- if (!found) {
- if (DBG) {
- Log.d(TAG, "getTimeZonesWithUniqueOffsets: add unique offset=" +
- zone.getRawOffset() + " zone.getID=" + zone.getID());
- }
- uniqueTimeZones.add(zone);
- }
- }
-
- synchronized(sLastUniqueLockObj) {
- // Cache the last result
- sLastUniqueZoneOffsets = extractZoneIds(uniqueTimeZones);
- sLastUniqueCountry = country;
-
- return sLastUniqueZoneOffsets;
- }
- }
-
- private static List<String> extractZoneIds(List<android.icu.util.TimeZone> timeZones) {
- List<String> ids = new ArrayList<>(timeZones.size());
- for (android.icu.util.TimeZone timeZone : timeZones) {
- ids.add(timeZone.getID());
- }
- return Collections.unmodifiableList(ids);
- }
-
- /**
- * Returns an immutable list of frozen ICU time zones for the country.
- *
- * @param countryIso is a two character country code.
- * @return TimeZone list, maybe empty but never null.
- * @hide
- */
- private static List<android.icu.util.TimeZone> getIcuTimeZones(String countryIso) {
- if (countryIso == null) {
- if (DBG) Log.d(TAG, "getIcuTimeZones(null): return empty list");
- return Collections.emptyList();
- }
- List<android.icu.util.TimeZone> timeZones =
- TimeZoneFinder.getInstance().lookupTimeZonesByCountry(countryIso);
- if (timeZones == null) {
- if (DBG) {
- Log.d(TAG, "getIcuTimeZones(" + countryIso
- + "): returned null, converting to empty list");
- }
- return Collections.emptyList();
- }
- return timeZones;
- }
-
- /**
* Returns a String indicating the version of the time zone database currently
* in use. The format of the string is dependent on the underlying time zone
* database implementation, but will typically contain the year in which the database
diff --git a/android/util/apk/ApkSignatureSchemeV2Verifier.java b/android/util/apk/ApkSignatureSchemeV2Verifier.java
index 530937e..5a09dab 100644
--- a/android/util/apk/ApkSignatureSchemeV2Verifier.java
+++ b/android/util/apk/ApkSignatureSchemeV2Verifier.java
@@ -16,6 +16,7 @@
package android.util.apk;
+import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_DSA_WITH_SHA256;
import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_ECDSA_WITH_SHA256;
import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_ECDSA_WITH_SHA512;
@@ -23,6 +24,9 @@
import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512;
import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PSS_WITH_SHA256;
import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PSS_WITH_SHA512;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_VERITY_DSA_WITH_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_VERITY_ECDSA_WITH_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256;
import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.getContentDigestAlgorithmJcaDigestAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
@@ -35,11 +39,11 @@
import android.util.Pair;
import java.io.ByteArrayInputStream;
-import java.io.FileDescriptor;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
+import java.security.DigestException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
@@ -103,7 +107,8 @@
*/
public static X509Certificate[][] verify(String apkFile)
throws SignatureNotFoundException, SecurityException, IOException {
- return verify(apkFile, true);
+ VerifiedSigner vSigner = verify(apkFile, true);
+ return vSigner.certs;
}
/**
@@ -117,10 +122,11 @@
*/
public static X509Certificate[][] plsCertsNoVerifyOnlyCerts(String apkFile)
throws SignatureNotFoundException, SecurityException, IOException {
- return verify(apkFile, false);
+ VerifiedSigner vSigner = verify(apkFile, false);
+ return vSigner.certs;
}
- private static X509Certificate[][] verify(String apkFile, boolean verifyIntegrity)
+ private static VerifiedSigner verify(String apkFile, boolean verifyIntegrity)
throws SignatureNotFoundException, SecurityException, IOException {
try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
return verify(apk, verifyIntegrity);
@@ -136,10 +142,10 @@
* verify.
* @throws IOException if an I/O error occurs while reading the APK file.
*/
- private static X509Certificate[][] verify(RandomAccessFile apk, boolean verifyIntegrity)
+ private static VerifiedSigner verify(RandomAccessFile apk, boolean verifyIntegrity)
throws SignatureNotFoundException, SecurityException, IOException {
SignatureInfo signatureInfo = findSignature(apk);
- return verify(apk.getFD(), signatureInfo, verifyIntegrity);
+ return verify(apk, signatureInfo, verifyIntegrity);
}
/**
@@ -161,10 +167,10 @@
* @param signatureInfo APK Signature Scheme v2 Block and information relevant for verifying it
* against the APK file.
*/
- private static X509Certificate[][] verify(
- FileDescriptor apkFileDescriptor,
+ private static VerifiedSigner verify(
+ RandomAccessFile apk,
SignatureInfo signatureInfo,
- boolean doVerifyIntegrity) throws SecurityException {
+ boolean doVerifyIntegrity) throws SecurityException, IOException {
int signerCount = 0;
Map<Integer, byte[]> contentDigests = new ArrayMap<>();
List<X509Certificate[]> signerCerts = new ArrayList<>();
@@ -202,16 +208,17 @@
}
if (doVerifyIntegrity) {
- ApkSigningBlockUtils.verifyIntegrity(
- contentDigests,
- apkFileDescriptor,
- signatureInfo.apkSigningBlockOffset,
- signatureInfo.centralDirOffset,
- signatureInfo.eocdOffset,
- signatureInfo.eocd);
+ ApkSigningBlockUtils.verifyIntegrity(contentDigests, apk, signatureInfo);
}
- return signerCerts.toArray(new X509Certificate[signerCerts.size()][]);
+ byte[] verityRootHash = null;
+ if (contentDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) {
+ verityRootHash = contentDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256);
+ }
+
+ return new VerifiedSigner(
+ signerCerts.toArray(new X509Certificate[signerCerts.size()][]),
+ verityRootHash);
}
private static X509Certificate[] verifySigner(
@@ -386,6 +393,25 @@
}
return;
}
+
+ static byte[] getVerityRootHash(String apkPath)
+ throws IOException, SignatureNotFoundException, SecurityException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ SignatureInfo signatureInfo = findSignature(apk);
+ VerifiedSigner vSigner = verify(apk, false);
+ return vSigner.verityRootHash;
+ }
+ }
+
+ static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory)
+ throws IOException, SignatureNotFoundException, SecurityException, DigestException,
+ NoSuchAlgorithmException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ SignatureInfo signatureInfo = findSignature(apk);
+ return ApkSigningBlockUtils.generateApkVerity(apkPath, bufferFactory, signatureInfo);
+ }
+ }
+
private static boolean isSupportedSignatureAlgorithm(int sigAlgorithm) {
switch (sigAlgorithm) {
case SIGNATURE_RSA_PSS_WITH_SHA256:
@@ -395,9 +421,28 @@
case SIGNATURE_ECDSA_WITH_SHA256:
case SIGNATURE_ECDSA_WITH_SHA512:
case SIGNATURE_DSA_WITH_SHA256:
+ case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_VERITY_ECDSA_WITH_SHA256:
+ case SIGNATURE_VERITY_DSA_WITH_SHA256:
return true;
default:
return false;
}
}
+
+ /**
+ * Verified APK Signature Scheme v2 signer.
+ *
+ * @hide for internal use only.
+ */
+ public static class VerifiedSigner {
+ public final X509Certificate[][] certs;
+ public final byte[] verityRootHash;
+
+ public VerifiedSigner(X509Certificate[][] certs, byte[] verityRootHash) {
+ this.certs = certs;
+ this.verityRootHash = verityRootHash;
+ }
+
+ }
}
diff --git a/android/util/apk/ApkSignatureSchemeV3Verifier.java b/android/util/apk/ApkSignatureSchemeV3Verifier.java
index e43dee3..1b04eb2 100644
--- a/android/util/apk/ApkSignatureSchemeV3Verifier.java
+++ b/android/util/apk/ApkSignatureSchemeV3Verifier.java
@@ -16,6 +16,7 @@
package android.util.apk;
+import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_DSA_WITH_SHA256;
import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_ECDSA_WITH_SHA256;
import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_ECDSA_WITH_SHA512;
@@ -23,6 +24,9 @@
import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512;
import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PSS_WITH_SHA256;
import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PSS_WITH_SHA512;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_VERITY_DSA_WITH_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_VERITY_ECDSA_WITH_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256;
import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.getContentDigestAlgorithmJcaDigestAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
@@ -36,11 +40,11 @@
import android.util.Pair;
import java.io.ByteArrayInputStream;
-import java.io.FileDescriptor;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
+import java.security.DigestException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
@@ -136,7 +140,7 @@
private static VerifiedSigner verify(RandomAccessFile apk, boolean verifyIntegrity)
throws SignatureNotFoundException, SecurityException, IOException {
SignatureInfo signatureInfo = findSignature(apk);
- return verify(apk.getFD(), signatureInfo, verifyIntegrity);
+ return verify(apk, signatureInfo, verifyIntegrity);
}
/**
@@ -159,7 +163,7 @@
* against the APK file.
*/
private static VerifiedSigner verify(
- FileDescriptor apkFileDescriptor,
+ RandomAccessFile apk,
SignatureInfo signatureInfo,
boolean doVerifyIntegrity) throws SecurityException {
int signerCount = 0;
@@ -206,13 +210,11 @@
}
if (doVerifyIntegrity) {
- ApkSigningBlockUtils.verifyIntegrity(
- contentDigests,
- apkFileDescriptor,
- signatureInfo.apkSigningBlockOffset,
- signatureInfo.centralDirOffset,
- signatureInfo.eocdOffset,
- signatureInfo.eocd);
+ ApkSigningBlockUtils.verifyIntegrity(contentDigests, apk, signatureInfo);
+ }
+
+ if (contentDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) {
+ result.verityRootHash = contentDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256);
}
return result;
@@ -503,6 +505,24 @@
return new VerifiedProofOfRotation(certs, flagsList);
}
+ static byte[] getVerityRootHash(String apkPath)
+ throws IOException, SignatureNotFoundException, SecurityException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ SignatureInfo signatureInfo = findSignature(apk);
+ VerifiedSigner vSigner = verify(apk, false);
+ return vSigner.verityRootHash;
+ }
+ }
+
+ static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory)
+ throws IOException, SignatureNotFoundException, SecurityException, DigestException,
+ NoSuchAlgorithmException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ SignatureInfo signatureInfo = findSignature(apk);
+ return ApkSigningBlockUtils.generateApkVerity(apkPath, bufferFactory, signatureInfo);
+ }
+ }
+
private static boolean isSupportedSignatureAlgorithm(int sigAlgorithm) {
switch (sigAlgorithm) {
case SIGNATURE_RSA_PSS_WITH_SHA256:
@@ -512,6 +532,9 @@
case SIGNATURE_ECDSA_WITH_SHA256:
case SIGNATURE_ECDSA_WITH_SHA512:
case SIGNATURE_DSA_WITH_SHA256:
+ case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_VERITY_ECDSA_WITH_SHA256:
+ case SIGNATURE_VERITY_DSA_WITH_SHA256:
return true;
default:
return false;
@@ -542,6 +565,8 @@
public final X509Certificate[] certs;
public final VerifiedProofOfRotation por;
+ public byte[] verityRootHash;
+
public VerifiedSigner(X509Certificate[] certs, VerifiedProofOfRotation por) {
this.certs = certs;
this.por = por;
diff --git a/android/util/apk/ApkSignatureVerifier.java b/android/util/apk/ApkSignatureVerifier.java
index 8146729..8794372 100644
--- a/android/util/apk/ApkSignatureVerifier.java
+++ b/android/util/apk/ApkSignatureVerifier.java
@@ -25,6 +25,7 @@
import android.content.pm.PackageParser;
import android.content.pm.PackageParser.PackageParserException;
+import android.content.pm.PackageParser.SigningDetails.SignatureSchemeVersion;
import android.content.pm.Signature;
import android.os.Trace;
import android.util.jar.StrictJarFile;
@@ -35,7 +36,9 @@
import java.io.IOException;
import java.io.InputStream;
+import java.security.DigestException;
import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
@@ -52,10 +55,6 @@
*/
public class ApkSignatureVerifier {
- public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
- public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
- public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
-
private static final AtomicReference<byte[]> sBuffer = new AtomicReference<>();
/**
@@ -63,10 +62,11 @@
*
* @throws PackageParserException if the APK's signature failed to verify.
*/
- public static Result verify(String apkPath, int minSignatureSchemeVersion)
+ public static PackageParser.SigningDetails verify(String apkPath,
+ @SignatureSchemeVersion int minSignatureSchemeVersion)
throws PackageParserException {
- if (minSignatureSchemeVersion > VERSION_APK_SIGNATURE_SCHEME_V3) {
+ if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V3) {
// V3 and before are older than the requested minimum signing version
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No signature found in package of version " + minSignatureSchemeVersion
@@ -80,10 +80,23 @@
ApkSignatureSchemeV3Verifier.verify(apkPath);
Certificate[][] signerCerts = new Certificate[][] { vSigner.certs };
Signature[] signerSigs = convertToSignatures(signerCerts);
- return new Result(signerCerts, signerSigs, VERSION_APK_SIGNATURE_SCHEME_V3);
+ Signature[] pastSignerSigs = null;
+ int[] pastSignerSigsFlags = null;
+ if (vSigner.por != null) {
+ // populate proof-of-rotation information
+ pastSignerSigs = new Signature[vSigner.por.certs.size()];
+ pastSignerSigsFlags = new int[vSigner.por.flagsList.size()];
+ for (int i = 0; i < pastSignerSigs.length; i++) {
+ pastSignerSigs[i] = new Signature(vSigner.por.certs.get(i).getEncoded());
+ pastSignerSigsFlags[i] = vSigner.por.flagsList.get(i);
+ }
+ }
+ return new PackageParser.SigningDetails(
+ signerSigs, SignatureSchemeVersion.SIGNING_BLOCK_V3,
+ pastSignerSigs, pastSignerSigsFlags);
} catch (SignatureNotFoundException e) {
- // not signed with v2, try older if allowed
- if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V3) {
+ // not signed with v3, try older if allowed
+ if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V3) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No APK Signature Scheme v3 signature in package " + apkPath, e);
}
@@ -91,13 +104,13 @@
// APK Signature Scheme v2 signature found but did not verify
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Failed to collect certificates from " + apkPath
- + " using APK Signature Scheme v2", e);
+ + " using APK Signature Scheme v3", e);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
// redundant, protective version check
- if (minSignatureSchemeVersion > VERSION_APK_SIGNATURE_SCHEME_V2) {
+ if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V2) {
// V2 and before are older than the requested minimum signing version
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No signature found in package of version " + minSignatureSchemeVersion
@@ -110,10 +123,11 @@
Certificate[][] signerCerts = ApkSignatureSchemeV2Verifier.verify(apkPath);
Signature[] signerSigs = convertToSignatures(signerCerts);
- return new Result(signerCerts, signerSigs, VERSION_APK_SIGNATURE_SCHEME_V2);
+ return new PackageParser.SigningDetails(
+ signerSigs, SignatureSchemeVersion.SIGNING_BLOCK_V2);
} catch (SignatureNotFoundException e) {
// not signed with v2, try older if allowed
- if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V2) {
+ if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V2) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No APK Signature Scheme v2 signature in package " + apkPath, e);
}
@@ -127,7 +141,7 @@
}
// redundant, protective version check
- if (minSignatureSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME) {
+ if (minSignatureSchemeVersion > SignatureSchemeVersion.JAR) {
// V1 and is older than the requested minimum signing version
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No signature found in package of version " + minSignatureSchemeVersion
@@ -145,7 +159,8 @@
*
* @throws PackageParserException if there was a problem collecting certificates
*/
- private static Result verifyV1Signature(String apkPath, boolean verifyFull)
+ private static PackageParser.SigningDetails verifyV1Signature(
+ String apkPath, boolean verifyFull)
throws PackageParserException {
StrictJarFile jarFile = null;
@@ -211,7 +226,7 @@
}
}
}
- return new Result(lastCerts, lastSigs, VERSION_JAR_SIGNATURE_SCHEME);
+ return new PackageParser.SigningDetails(lastSigs, SignatureSchemeVersion.JAR);
} catch (GeneralSecurityException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
"Failed to collect certificates from " + apkPath, e);
@@ -289,10 +304,11 @@
* @throws PackageParserException if the APK's signature failed to verify.
* or greater is not found, except in the case of no JAR signature.
*/
- public static Result plsCertsNoVerifyOnlyCerts(String apkPath, int minSignatureSchemeVersion)
+ public static PackageParser.SigningDetails plsCertsNoVerifyOnlyCerts(
+ String apkPath, int minSignatureSchemeVersion)
throws PackageParserException {
- if (minSignatureSchemeVersion > VERSION_APK_SIGNATURE_SCHEME_V3) {
+ if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V3) {
// V3 and before are older than the requested minimum signing version
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No signature found in package of version " + minSignatureSchemeVersion
@@ -300,30 +316,43 @@
}
// first try v3
- Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV3");
+ Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "certsOnlyV3");
try {
ApkSignatureSchemeV3Verifier.VerifiedSigner vSigner =
ApkSignatureSchemeV3Verifier.plsCertsNoVerifyOnlyCerts(apkPath);
Certificate[][] signerCerts = new Certificate[][] { vSigner.certs };
Signature[] signerSigs = convertToSignatures(signerCerts);
- return new Result(signerCerts, signerSigs, VERSION_APK_SIGNATURE_SCHEME_V3);
+ Signature[] pastSignerSigs = null;
+ int[] pastSignerSigsFlags = null;
+ if (vSigner.por != null) {
+ // populate proof-of-rotation information
+ pastSignerSigs = new Signature[vSigner.por.certs.size()];
+ pastSignerSigsFlags = new int[vSigner.por.flagsList.size()];
+ for (int i = 0; i < pastSignerSigs.length; i++) {
+ pastSignerSigs[i] = new Signature(vSigner.por.certs.get(i).getEncoded());
+ pastSignerSigsFlags[i] = vSigner.por.flagsList.get(i);
+ }
+ }
+ return new PackageParser.SigningDetails(
+ signerSigs, SignatureSchemeVersion.SIGNING_BLOCK_V3,
+ pastSignerSigs, pastSignerSigsFlags);
} catch (SignatureNotFoundException e) {
- // not signed with v2, try older if allowed
- if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V3) {
+ // not signed with v3, try older if allowed
+ if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V3) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No APK Signature Scheme v3 signature in package " + apkPath, e);
}
} catch (Exception e) {
- // APK Signature Scheme v2 signature found but did not verify
+ // APK Signature Scheme v3 signature found but did not verify
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Failed to collect certificates from " + apkPath
- + " using APK Signature Scheme v2", e);
+ + " using APK Signature Scheme v3", e);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
// redundant, protective version check
- if (minSignatureSchemeVersion > VERSION_APK_SIGNATURE_SCHEME_V2) {
+ if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V2) {
// V2 and before are older than the requested minimum signing version
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No signature found in package of version " + minSignatureSchemeVersion
@@ -336,10 +365,11 @@
Certificate[][] signerCerts =
ApkSignatureSchemeV2Verifier.plsCertsNoVerifyOnlyCerts(apkPath);
Signature[] signerSigs = convertToSignatures(signerCerts);
- return new Result(signerCerts, signerSigs, VERSION_APK_SIGNATURE_SCHEME_V2);
+ return new PackageParser.SigningDetails(signerSigs,
+ SignatureSchemeVersion.SIGNING_BLOCK_V2);
} catch (SignatureNotFoundException e) {
// not signed with v2, try older if allowed
- if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V2) {
+ if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V2) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No APK Signature Scheme v2 signature in package " + apkPath, e);
}
@@ -353,7 +383,7 @@
}
// redundant, protective version check
- if (minSignatureSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME) {
+ if (minSignatureSchemeVersion > SignatureSchemeVersion.JAR) {
// V1 and is older than the requested minimum signing version
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No signature found in package of version " + minSignatureSchemeVersion
@@ -365,6 +395,38 @@
}
/**
+ * @return the verity root hash in the Signing Block.
+ */
+ public static byte[] getVerityRootHash(String apkPath)
+ throws IOException, SignatureNotFoundException, SecurityException {
+ // first try v3
+ try {
+ return ApkSignatureSchemeV3Verifier.getVerityRootHash(apkPath);
+ } catch (SignatureNotFoundException e) {
+ // try older version
+ }
+ return ApkSignatureSchemeV2Verifier.getVerityRootHash(apkPath);
+ }
+
+ /**
+ * Generates the Merkle tree and verity metadata to the buffer allocated by the {@code
+ * ByteBufferFactory}.
+ *
+ * @return the verity root hash of the generated Merkle tree.
+ */
+ public static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory)
+ throws IOException, SignatureNotFoundException, SecurityException, DigestException,
+ NoSuchAlgorithmException {
+ // first try v3
+ try {
+ return ApkSignatureSchemeV3Verifier.generateApkVerity(apkPath, bufferFactory);
+ } catch (SignatureNotFoundException e) {
+ // try older version
+ }
+ return ApkSignatureSchemeV2Verifier.generateApkVerity(apkPath, bufferFactory);
+ }
+
+ /**
* Result of a successful APK verification operation.
*/
public static class Result {
diff --git a/android/util/apk/ApkSigningBlockUtils.java b/android/util/apk/ApkSigningBlockUtils.java
index 9279510..4146f6f 100644
--- a/android/util/apk/ApkSigningBlockUtils.java
+++ b/android/util/apk/ApkSigningBlockUtils.java
@@ -16,6 +16,7 @@
package android.util.apk;
+import android.util.ArrayMap;
import android.util.Pair;
import java.io.FileDescriptor;
@@ -30,6 +31,7 @@
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
+import java.util.Arrays;
import java.util.Map;
/**
@@ -84,16 +86,41 @@
static void verifyIntegrity(
Map<Integer, byte[]> expectedDigests,
- FileDescriptor apkFileDescriptor,
- long apkSigningBlockOffset,
- long centralDirOffset,
- long eocdOffset,
- ByteBuffer eocdBuf) throws SecurityException {
-
+ RandomAccessFile apk,
+ SignatureInfo signatureInfo) throws SecurityException {
if (expectedDigests.isEmpty()) {
throw new SecurityException("No digests provided");
}
+ Map<Integer, byte[]> expected1MbChunkDigests = new ArrayMap<>();
+ if (expectedDigests.containsKey(CONTENT_DIGEST_CHUNKED_SHA256)) {
+ expected1MbChunkDigests.put(CONTENT_DIGEST_CHUNKED_SHA256,
+ expectedDigests.get(CONTENT_DIGEST_CHUNKED_SHA256));
+ }
+ if (expectedDigests.containsKey(CONTENT_DIGEST_CHUNKED_SHA512)) {
+ expected1MbChunkDigests.put(CONTENT_DIGEST_CHUNKED_SHA512,
+ expectedDigests.get(CONTENT_DIGEST_CHUNKED_SHA512));
+ }
+
+ if (expectedDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) {
+ verifyIntegrityForVerityBasedAlgorithm(
+ expectedDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256), apk, signatureInfo);
+ } else if (!expected1MbChunkDigests.isEmpty()) {
+ try {
+ verifyIntegrityFor1MbChunkBasedAlgorithm(expected1MbChunkDigests, apk.getFD(),
+ signatureInfo);
+ } catch (IOException e) {
+ throw new SecurityException("Cannot get FD", e);
+ }
+ } else {
+ throw new SecurityException("No known digest exists for integrity check");
+ }
+ }
+
+ private static void verifyIntegrityFor1MbChunkBasedAlgorithm(
+ Map<Integer, byte[]> expectedDigests,
+ FileDescriptor apkFileDescriptor,
+ SignatureInfo signatureInfo) throws SecurityException {
// We need to verify the integrity of the following three sections of the file:
// 1. Everything up to the start of the APK Signing Block.
// 2. ZIP Central Directory.
@@ -105,16 +132,18 @@
// APK are already there in the OS's page cache and thus mmap does not use additional
// physical memory.
DataSource beforeApkSigningBlock =
- new MemoryMappedFileDataSource(apkFileDescriptor, 0, apkSigningBlockOffset);
+ new MemoryMappedFileDataSource(apkFileDescriptor, 0,
+ signatureInfo.apkSigningBlockOffset);
DataSource centralDir =
new MemoryMappedFileDataSource(
- apkFileDescriptor, centralDirOffset, eocdOffset - centralDirOffset);
+ apkFileDescriptor, signatureInfo.centralDirOffset,
+ signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
// For the purposes of integrity verification, ZIP End of Central Directory's field Start of
// Central Directory must be considered to point to the offset of the APK Signing Block.
- eocdBuf = eocdBuf.duplicate();
+ ByteBuffer eocdBuf = signatureInfo.eocd.duplicate();
eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
- ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, apkSigningBlockOffset);
+ ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, signatureInfo.apkSigningBlockOffset);
DataSource eocd = new ByteBufferDataSource(eocdBuf);
int[] digestAlgorithms = new int[expectedDigests.size()];
@@ -126,7 +155,7 @@
byte[][] actualDigests;
try {
actualDigests =
- computeContentDigests(
+ computeContentDigestsPer1MbChunk(
digestAlgorithms,
new DataSource[] {beforeApkSigningBlock, centralDir, eocd});
} catch (DigestException e) {
@@ -144,7 +173,7 @@
}
}
- private static byte[][] computeContentDigests(
+ private static byte[][] computeContentDigestsPer1MbChunk(
int[] digestAlgorithms,
DataSource[] contents) throws DigestException {
// For each digest algorithm the result is computed as follows:
@@ -256,6 +285,46 @@
return result;
}
+ private static void verifyIntegrityForVerityBasedAlgorithm(
+ byte[] expectedRootHash,
+ RandomAccessFile apk,
+ SignatureInfo signatureInfo) throws SecurityException {
+ try {
+ ApkVerityBuilder.ApkVerityResult verity = ApkVerityBuilder.generateApkVerity(apk,
+ signatureInfo, new ByteBufferFactory() {
+ @Override
+ public ByteBuffer create(int capacity) {
+ return ByteBuffer.allocate(capacity);
+ }
+ });
+ if (!Arrays.equals(expectedRootHash, verity.rootHash)) {
+ throw new SecurityException("APK verity digest of contents did not verify");
+ }
+ } catch (DigestException | IOException | NoSuchAlgorithmException e) {
+ throw new SecurityException("Error during verification", e);
+ }
+ }
+
+ /**
+ * Generates the fsverity header and hash tree to be used by kernel for the given apk. This
+ * method does not check whether the root hash exists in the Signing Block or not.
+ *
+ * <p>The output is stored in the {@link ByteBuffer} created by the given {@link
+ * ByteBufferFactory}.
+ *
+ * @return the root hash of the generated hash tree.
+ */
+ public static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory,
+ SignatureInfo signatureInfo)
+ throws IOException, SignatureNotFoundException, SecurityException, DigestException,
+ NoSuchAlgorithmException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ ApkVerityBuilder.ApkVerityResult result = ApkVerityBuilder.generateApkVerity(apk,
+ signatureInfo, bufferFactory);
+ return result.rootHash;
+ }
+ }
+
/**
* Returns the ZIP End of Central Directory (EoCD) and its offset in the file.
*
@@ -304,9 +373,13 @@
static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201;
static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202;
static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301;
+ static final int SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0401;
+ static final int SIGNATURE_VERITY_ECDSA_WITH_SHA256 = 0x0403;
+ static final int SIGNATURE_VERITY_DSA_WITH_SHA256 = 0x0405;
static final int CONTENT_DIGEST_CHUNKED_SHA256 = 1;
static final int CONTENT_DIGEST_CHUNKED_SHA512 = 2;
+ static final int CONTENT_DIGEST_VERITY_CHUNKED_SHA256 = 3;
static int compareSignatureAlgorithm(int sigAlgorithm1, int sigAlgorithm2) {
int digestAlgorithm1 = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm1);
@@ -321,6 +394,7 @@
case CONTENT_DIGEST_CHUNKED_SHA256:
return 0;
case CONTENT_DIGEST_CHUNKED_SHA512:
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
return -1;
default:
throw new IllegalArgumentException(
@@ -329,6 +403,7 @@
case CONTENT_DIGEST_CHUNKED_SHA512:
switch (digestAlgorithm2) {
case CONTENT_DIGEST_CHUNKED_SHA256:
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
return 1;
case CONTENT_DIGEST_CHUNKED_SHA512:
return 0;
@@ -336,6 +411,18 @@
throw new IllegalArgumentException(
"Unknown digestAlgorithm2: " + digestAlgorithm2);
}
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
+ switch (digestAlgorithm2) {
+ case CONTENT_DIGEST_CHUNKED_SHA512:
+ return -1;
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
+ return 0;
+ case CONTENT_DIGEST_CHUNKED_SHA256:
+ return 1;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown digestAlgorithm2: " + digestAlgorithm2);
+ }
default:
throw new IllegalArgumentException("Unknown digestAlgorithm1: " + digestAlgorithm1);
}
@@ -352,6 +439,10 @@
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
case SIGNATURE_ECDSA_WITH_SHA512:
return CONTENT_DIGEST_CHUNKED_SHA512;
+ case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_VERITY_ECDSA_WITH_SHA256:
+ case SIGNATURE_VERITY_DSA_WITH_SHA256:
+ return CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
default:
throw new IllegalArgumentException(
"Unknown signature algorithm: 0x"
@@ -362,6 +453,7 @@
static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) {
switch (digestAlgorithm) {
case CONTENT_DIGEST_CHUNKED_SHA256:
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
return "SHA-256";
case CONTENT_DIGEST_CHUNKED_SHA512:
return "SHA-512";
@@ -374,6 +466,7 @@
private static int getContentDigestAlgorithmOutputSizeBytes(int digestAlgorithm) {
switch (digestAlgorithm) {
case CONTENT_DIGEST_CHUNKED_SHA256:
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
return 256 / 8;
case CONTENT_DIGEST_CHUNKED_SHA512:
return 512 / 8;
@@ -389,11 +482,14 @@
case SIGNATURE_RSA_PSS_WITH_SHA512:
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
+ case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256:
return "RSA";
case SIGNATURE_ECDSA_WITH_SHA256:
case SIGNATURE_ECDSA_WITH_SHA512:
+ case SIGNATURE_VERITY_ECDSA_WITH_SHA256:
return "EC";
case SIGNATURE_DSA_WITH_SHA256:
+ case SIGNATURE_VERITY_DSA_WITH_SHA256:
return "DSA";
default:
throw new IllegalArgumentException(
@@ -416,14 +512,17 @@
new PSSParameterSpec(
"SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1));
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256:
return Pair.create("SHA256withRSA", null);
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
return Pair.create("SHA512withRSA", null);
case SIGNATURE_ECDSA_WITH_SHA256:
+ case SIGNATURE_VERITY_ECDSA_WITH_SHA256:
return Pair.create("SHA256withECDSA", null);
case SIGNATURE_ECDSA_WITH_SHA512:
return Pair.create("SHA512withECDSA", null);
case SIGNATURE_DSA_WITH_SHA256:
+ case SIGNATURE_VERITY_DSA_WITH_SHA256:
return Pair.create("SHA256withDSA", null);
default:
throw new IllegalArgumentException(
diff --git a/android/util/apk/ApkVerityBuilder.java b/android/util/apk/ApkVerityBuilder.java
index 7412ef4..ba21ccb 100644
--- a/android/util/apk/ApkVerityBuilder.java
+++ b/android/util/apk/ApkVerityBuilder.java
@@ -68,31 +68,80 @@
static ApkVerityResult generateApkVerity(RandomAccessFile apk,
SignatureInfo signatureInfo, ByteBufferFactory bufferFactory)
throws IOException, SecurityException, NoSuchAlgorithmException, DigestException {
+ long signingBlockSize =
+ signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset;
+ long dataSize = apk.length() - signingBlockSize;
+ int[] levelOffset = calculateVerityLevelOffset(dataSize);
+
+ ByteBuffer output = bufferFactory.create(
+ CHUNK_SIZE_BYTES + // fsverity header + extensions + padding
+ levelOffset[levelOffset.length - 1]); // Merkle tree size
+ output.order(ByteOrder.LITTLE_ENDIAN);
+
+ ByteBuffer header = slice(output, 0, FSVERITY_HEADER_SIZE_BYTES);
+ ByteBuffer extensions = slice(output, FSVERITY_HEADER_SIZE_BYTES, CHUNK_SIZE_BYTES);
+ ByteBuffer tree = slice(output, CHUNK_SIZE_BYTES, output.limit());
+ byte[] apkDigestBytes = new byte[DIGEST_SIZE_BYTES];
+ ByteBuffer apkDigest = ByteBuffer.wrap(apkDigestBytes);
+ apkDigest.order(ByteOrder.LITTLE_ENDIAN);
+
+ calculateFsveritySignatureInternal(apk, signatureInfo, tree, apkDigest, header, extensions);
+
+ output.rewind();
+ return new ApkVerityResult(output, apkDigestBytes);
+ }
+
+ /**
+ * Calculates the fsverity root hash for integrity measurement. This needs to be consistent to
+ * what kernel returns.
+ */
+ static byte[] generateFsverityRootHash(RandomAccessFile apk, ByteBuffer apkDigest,
+ SignatureInfo signatureInfo)
+ throws NoSuchAlgorithmException, DigestException, IOException {
+ ByteBuffer verityBlock = ByteBuffer.allocate(CHUNK_SIZE_BYTES)
+ .order(ByteOrder.LITTLE_ENDIAN);
+ ByteBuffer header = slice(verityBlock, 0, FSVERITY_HEADER_SIZE_BYTES);
+ ByteBuffer extensions = slice(verityBlock, FSVERITY_HEADER_SIZE_BYTES, CHUNK_SIZE_BYTES);
+
+ calculateFsveritySignatureInternal(apk, signatureInfo, null, null, header, extensions);
+
+ MessageDigest md = MessageDigest.getInstance(JCA_DIGEST_ALGORITHM);
+ md.update(DEFAULT_SALT);
+ md.update(verityBlock);
+ md.update(apkDigest);
+ return md.digest();
+ }
+
+ private static void calculateFsveritySignatureInternal(
+ RandomAccessFile apk, SignatureInfo signatureInfo, ByteBuffer treeOutput,
+ ByteBuffer rootHashOutput, ByteBuffer headerOutput, ByteBuffer extensionsOutput)
+ throws IOException, NoSuchAlgorithmException, DigestException {
assertSigningBlockAlignedAndHasFullPages(signatureInfo);
long signingBlockSize =
signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset;
long dataSize = apk.length() - signingBlockSize - ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE;
int[] levelOffset = calculateVerityLevelOffset(dataSize);
- ByteBuffer output = bufferFactory.create(
- CHUNK_SIZE_BYTES + // fsverity header + extensions + padding
- levelOffset[levelOffset.length - 1] + // Merkle tree size
- FSVERITY_HEADER_SIZE_BYTES); // second fsverity header (verbatim copy)
- // Start generating the tree from the block boundary as the kernel will expect.
- ByteBuffer treeOutput = slice(output, CHUNK_SIZE_BYTES,
- output.limit() - FSVERITY_HEADER_SIZE_BYTES);
- byte[] rootHash = generateApkVerityTree(apk, signatureInfo, DEFAULT_SALT, levelOffset,
- treeOutput);
+ if (treeOutput != null) {
+ byte[] apkRootHash = generateApkVerityTree(apk, signatureInfo, DEFAULT_SALT,
+ levelOffset, treeOutput);
+ if (rootHashOutput != null) {
+ rootHashOutput.put(apkRootHash);
+ }
+ }
- ByteBuffer integrityHeader = generateFsverityHeader(apk.length(), DEFAULT_SALT);
- output.put(integrityHeader);
- output.put(generateFsverityExtensions());
+ if (headerOutput != null) {
+ headerOutput.order(ByteOrder.LITTLE_ENDIAN);
+ generateFsverityHeader(headerOutput, apk.length(), levelOffset.length - 1,
+ DEFAULT_SALT);
+ }
- integrityHeader.rewind();
- output.put(integrityHeader);
- output.rewind();
- return new ApkVerityResult(output, rootHash);
+ if (extensionsOutput != null) {
+ extensionsOutput.order(ByteOrder.LITTLE_ENDIAN);
+ generateFsverityExtensions(extensionsOutput, signatureInfo.apkSigningBlockOffset,
+ signingBlockSize, signatureInfo.eocdOffset);
+ }
}
/**
@@ -164,11 +213,11 @@
}
private void fillUpLastOutputChunk() {
- int extra = (int) (BUFFER_SIZE - mOutput.position() % BUFFER_SIZE);
- if (extra == 0) {
+ int lastBlockSize = (int) (mOutput.position() % BUFFER_SIZE);
+ if (lastBlockSize == 0) {
return;
}
- mOutput.put(ByteBuffer.allocate(extra));
+ mOutput.put(ByteBuffer.allocate(BUFFER_SIZE - lastBlockSize));
}
}
@@ -211,7 +260,7 @@
eocdCdOffsetFieldPosition - signatureInfo.centralDirOffset),
MMAP_REGION_SIZE_BYTES);
- // 3. Fill up the rest of buffer with 0s.
+ // 3. Consume offset of Signing Block as an alternative EoCD.
ByteBuffer alternativeCentralDirOffset = ByteBuffer.allocate(
ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE).order(ByteOrder.LITTLE_ENDIAN);
alternativeCentralDirOffset.putInt(Math.toIntExact(signatureInfo.apkSigningBlockOffset));
@@ -259,36 +308,109 @@
return rootHash;
}
- private static ByteBuffer generateFsverityHeader(long fileSize, byte[] salt) {
+ private static void bufferPut(ByteBuffer buffer, byte value) {
+ // FIXME(b/72459251): buffer.put(value) does NOT work surprisingly. The position() after put
+ // does NOT even change. This hack workaround the problem, but the root cause remains
+ // unkonwn yet. This seems only happen when it goes through the apk install flow on my
+ // setup.
+ buffer.put(new byte[] { value });
+ }
+
+ private static ByteBuffer generateFsverityHeader(ByteBuffer buffer, long fileSize, int depth,
+ byte[] salt) {
if (salt.length != 8) {
throw new IllegalArgumentException("salt is not 8 bytes long");
}
- ByteBuffer buffer = ByteBuffer.allocate(FSVERITY_HEADER_SIZE_BYTES);
- buffer.order(ByteOrder.LITTLE_ENDIAN);
-
- // TODO(b/30972906): insert a reference when there is a public one.
+ // TODO(b/30972906): update the reference when there is a better one in public.
buffer.put("TrueBrew".getBytes()); // magic
- buffer.put((byte) 1); // major version
- buffer.put((byte) 0); // minor version
- buffer.put((byte) 12); // log2(block-size) == log2(4096)
- buffer.put((byte) 7); // log2(leaves-per-node) == log2(block-size / digest-size)
- // == log2(4096 / 32)
- buffer.putShort((short) 1); // meta algorithm, 1: SHA-256 FIXME finalize constant
- buffer.putShort((short) 1); // data algorithm, 1: SHA-256 FIXME finalize constant
- buffer.putInt(0x1); // flags, 0x1: has extension, FIXME also hide it
- buffer.putInt(0); // reserved
- buffer.putLong(fileSize); // original i_size
- buffer.put(salt); // salt (8 bytes)
- // TODO(b/30972906): Add extension.
+ bufferPut(buffer, (byte) 1); // major version
+ bufferPut(buffer, (byte) 0); // minor version
+ bufferPut(buffer, (byte) 12); // log2(block-size): log2(4096)
+ bufferPut(buffer, (byte) 7); // log2(leaves-per-node): log2(4096 / 32)
+
+ buffer.putShort((short) 1); // meta algorithm, SHA256_MODE == 1
+ buffer.putShort((short) 1); // data algorithm, SHA256_MODE == 1
+
+ buffer.putInt(0x1); // flags, 0x1: has extension
+ buffer.putInt(0); // reserved
+
+ buffer.putLong(fileSize); // original file size
+
+ bufferPut(buffer, (byte) 0); // auth block offset, disabled here
+ bufferPut(buffer, (byte) 2); // extension count
+ buffer.put(salt); // salt (8 bytes)
+ // skip(buffer, 22); // reserved
buffer.rewind();
return buffer;
}
- private static ByteBuffer generateFsverityExtensions() {
- return ByteBuffer.allocate(64); // TODO(b/30972906): implement this.
+ private static ByteBuffer generateFsverityExtensions(ByteBuffer buffer, long signingBlockOffset,
+ long signingBlockSize, long eocdOffset) {
+ // Snapshot of the FSVerity structs (subject to change once upstreamed).
+ //
+ // struct fsverity_extension {
+ // __le16 length;
+ // u8 type;
+ // u8 reserved[5];
+ // };
+ //
+ // struct fsverity_extension_elide {
+ // __le64 offset;
+ // __le64 length;
+ // }
+ //
+ // struct fsverity_extension_patch {
+ // __le64 offset;
+ // u8 length;
+ // u8 reserved[7];
+ // u8 databytes[];
+ // };
+
+ final int kSizeOfFsverityExtensionHeader = 8;
+
+ {
+ // struct fsverity_extension #1
+ final int kSizeOfFsverityElidedExtension = 16;
+
+ buffer.putShort((short) // total size of extension, padded to 64-bit alignment
+ (kSizeOfFsverityExtensionHeader + kSizeOfFsverityElidedExtension));
+ buffer.put((byte) 0); // ID of elide extension
+ skip(buffer, 5); // reserved
+
+ // struct fsverity_extension_elide
+ buffer.putLong(signingBlockOffset);
+ buffer.putLong(signingBlockSize);
+ }
+
+ {
+ // struct fsverity_extension #2
+ final int kSizeOfFsverityPatchExtension =
+ 8 + // offset size
+ 1 + // size of length from offset (up to 255)
+ 7 + // reserved
+ ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE;
+ final int kPadding = (int) divideRoundup(kSizeOfFsverityPatchExtension % 8, 8);
+
+ buffer.putShort((short) // total size of extension, padded to 64-bit alignment
+ (kSizeOfFsverityExtensionHeader + kSizeOfFsverityPatchExtension + kPadding));
+ buffer.put((byte) 1); // ID of patch extension
+ skip(buffer, 5); // reserved
+
+ // struct fsverity_extension_patch
+ buffer.putLong(eocdOffset); // offset
+ buffer.put((byte) ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE); // length
+ skip(buffer, 7); // reserved
+ buffer.putInt(Math.toIntExact(signingBlockOffset)); // databytes
+
+ // There are extra kPadding bytes of 0s here, included in the total size field of the
+ // extension header. The output ByteBuffer is assumed to be initialized to 0.
+ }
+
+ buffer.rewind();
+ return buffer;
}
/**
@@ -344,6 +466,11 @@
return b.slice();
}
+ /** Skip the {@code ByteBuffer} position by {@code bytes}. */
+ private static void skip(ByteBuffer buffer, int bytes) {
+ buffer.position(buffer.position() + bytes);
+ }
+
/** Divides a number and round up to the closest integer. */
private static long divideRoundup(long dividend, long divisor) {
return (dividend + divisor - 1) / divisor;
diff --git a/android/util/proto/ProtoUtils.java b/android/util/proto/ProtoUtils.java
index 85b7ec8..c7bbb9f 100644
--- a/android/util/proto/ProtoUtils.java
+++ b/android/util/proto/ProtoUtils.java
@@ -48,4 +48,26 @@
proto.write(Duration.END_MS, endMs);
proto.end(token);
}
+
+ /**
+ * Helper function to write bit-wise flags to proto as repeated enums
+ * @hide
+ */
+ public static void writeBitWiseFlagsToProtoEnum(ProtoOutputStream proto, long fieldId,
+ int flags, int[] origEnums, int[] protoEnums) {
+ if (protoEnums.length != origEnums.length) {
+ throw new IllegalArgumentException("The length of origEnums must match protoEnums");
+ }
+ int len = origEnums.length;
+ for (int i = 0; i < len; i++) {
+ // handle zero flag case.
+ if (origEnums[i] == 0 && flags == 0) {
+ proto.write(fieldId, protoEnums[i]);
+ return;
+ }
+ if ((flags & origEnums[i]) != 0) {
+ proto.write(fieldId, protoEnums[i]);
+ }
+ }
+ }
}
diff --git a/android/view/Choreographer.java b/android/view/Choreographer.java
index ba6b6cf..1caea57 100644
--- a/android/view/Choreographer.java
+++ b/android/view/Choreographer.java
@@ -235,6 +235,8 @@
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
+ // b/68769804: For low FPS experiments.
+ setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
}
private static float getRefreshRate() {
@@ -371,6 +373,7 @@
* @see #removeCallbacks
* @hide
*/
+ @TestApi
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
@@ -389,6 +392,7 @@
* @see #removeCallback
* @hide
*/
+ @TestApi
public void postCallbackDelayed(int callbackType,
Runnable action, Object token, long delayMillis) {
if (action == null) {
@@ -438,6 +442,7 @@
* @see #postCallbackDelayed
* @hide
*/
+ @TestApi
public void removeCallbacks(int callbackType, Runnable action, Object token) {
if (callbackType < 0 || callbackType > CALLBACK_LAST) {
throw new IllegalArgumentException("callbackType is invalid");
@@ -605,6 +610,7 @@
void setFPSDivisor(int divisor) {
if (divisor <= 0) divisor = 1;
mFPSDivisor = divisor;
+ ThreadedRenderer.setFPSDivisor(divisor);
}
void doFrame(long frameTimeNanos, int frame) {
diff --git a/android/view/DisplayCutout.java b/android/view/DisplayCutout.java
index e448f14..a61c8c1 100644
--- a/android/view/DisplayCutout.java
+++ b/android/view/DisplayCutout.java
@@ -16,11 +16,15 @@
package android.view;
+import static android.view.DisplayCutoutProto.BOUNDS;
+import static android.view.DisplayCutoutProto.INSETS;
import static android.view.Surface.ROTATION_0;
import static android.view.Surface.ROTATION_180;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
+import android.content.res.Resources;
+import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
@@ -28,7 +32,12 @@
import android.graphics.Region;
import android.os.Parcel;
import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.PathParser;
+import android.util.proto.ProtoOutputStream;
+import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import java.util.List;
@@ -40,6 +49,9 @@
*/
public final class DisplayCutout {
+ private static final String TAG = "DisplayCutout";
+ private static final String DP_MARKER = "@dp";
+
private static final Rect ZERO_RECT = new Rect();
private static final Region EMPTY_REGION = new Region();
@@ -150,11 +162,21 @@
@Override
public String toString() {
return "DisplayCutout{insets=" + mSafeInsets
- + " bounds=" + mBounds
+ + " boundingRect=" + getBoundingRect()
+ "}";
}
/**
+ * @hide
+ */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ mSafeInsets.writeToProto(proto, INSETS);
+ mBounds.getBounds().writeToProto(proto, BOUNDS);
+ proto.end(token);
+ }
+
+ /**
* Insets the reference frame of the cutout in the given directions.
*
* @return a copy of this instance which has been inset
@@ -266,9 +288,7 @@
* @hide
*/
public static DisplayCutout fromBoundingPolygon(List<Point> points) {
- Region bounds = Region.obtain();
Path path = new Path();
-
path.reset();
for (int i = 0; i < points.size(); i++) {
Point point = points.get(i);
@@ -279,18 +299,62 @@
}
}
path.close();
+ return fromBounds(path);
+ }
+ /**
+ * Creates an instance from a bounding {@link Path}.
+ *
+ * @hide
+ */
+ public static DisplayCutout fromBounds(Path path) {
RectF clipRect = new RectF();
path.computeBounds(clipRect, false /* unused */);
Region clipRegion = Region.obtain();
clipRegion.set((int) clipRect.left, (int) clipRect.top,
(int) clipRect.right, (int) clipRect.bottom);
+ Region bounds = new Region();
bounds.setPath(path, clipRegion);
+ clipRegion.recycle();
return new DisplayCutout(ZERO_RECT, bounds);
}
/**
+ * Creates an instance according to @android:string/config_mainBuiltInDisplayCutout.
+ *
+ * @hide
+ */
+ public static DisplayCutout fromResources(Resources res, int displayWidth) {
+ String spec = res.getString(R.string.config_mainBuiltInDisplayCutout);
+ if (TextUtils.isEmpty(spec)) {
+ return null;
+ }
+ spec = spec.trim();
+ final boolean inDp = spec.endsWith(DP_MARKER);
+ if (inDp) {
+ spec = spec.substring(0, spec.length() - DP_MARKER.length());
+ }
+
+ Path p;
+ try {
+ p = PathParser.createPathFromPathData(spec);
+ } catch (Throwable e) {
+ Log.wtf(TAG, "Could not inflate cutout: ", e);
+ return null;
+ }
+
+ final Matrix m = new Matrix();
+ if (inDp) {
+ final float dpToPx = res.getDisplayMetrics().density;
+ m.postScale(dpToPx, dpToPx);
+ }
+ m.postTranslate(displayWidth / 2f, 0);
+ p.transform(m);
+ return fromBounds(p);
+ }
+
+ /**
* Helper class for passing {@link DisplayCutout} through binder.
*
* Needed, because {@code readFromParcel} cannot be used with immutable classes.
@@ -316,12 +380,23 @@
@Override
public void writeToParcel(Parcel out, int flags) {
- if (mInner == NO_CUTOUT) {
+ writeCutoutToParcel(mInner, out, flags);
+ }
+
+ /**
+ * Writes a DisplayCutout to a {@link Parcel}.
+ *
+ * @see #readCutoutFromParcel(Parcel)
+ */
+ public static void writeCutoutToParcel(DisplayCutout cutout, Parcel out, int flags) {
+ if (cutout == null) {
+ out.writeInt(-1);
+ } else if (cutout == NO_CUTOUT) {
out.writeInt(0);
} else {
out.writeInt(1);
- out.writeTypedObject(mInner.mSafeInsets, flags);
- out.writeTypedObject(mInner.mBounds, flags);
+ out.writeTypedObject(cutout.mSafeInsets, flags);
+ out.writeTypedObject(cutout.mBounds, flags);
}
}
@@ -332,13 +407,13 @@
* Needed for AIDL out parameters.
*/
public void readFromParcel(Parcel in) {
- mInner = readCutout(in);
+ mInner = readCutoutFromParcel(in);
}
public static final Creator<ParcelableWrapper> CREATOR = new Creator<ParcelableWrapper>() {
@Override
public ParcelableWrapper createFromParcel(Parcel in) {
- return new ParcelableWrapper(readCutout(in));
+ return new ParcelableWrapper(readCutoutFromParcel(in));
}
@Override
@@ -347,8 +422,17 @@
}
};
- private static DisplayCutout readCutout(Parcel in) {
- if (in.readInt() == 0) {
+ /**
+ * Reads a DisplayCutout from a {@link Parcel}.
+ *
+ * @see #writeCutoutToParcel(DisplayCutout, Parcel, int)
+ */
+ public static DisplayCutout readCutoutFromParcel(Parcel in) {
+ int variant = in.readInt();
+ if (variant == -1) {
+ return null;
+ }
+ if (variant == 0) {
return NO_CUTOUT;
}
diff --git a/android/view/DisplayInfo.java b/android/view/DisplayInfo.java
index b813ddb..37e9815 100644
--- a/android/view/DisplayInfo.java
+++ b/android/view/DisplayInfo.java
@@ -149,6 +149,13 @@
public int overscanBottom;
/**
+ * The {@link DisplayCutout} if present, otherwise {@code null}.
+ *
+ * @hide
+ */
+ public DisplayCutout displayCutout;
+
+ /**
* The rotation of the display relative to its natural orientation.
* May be one of {@link android.view.Surface#ROTATION_0},
* {@link android.view.Surface#ROTATION_90}, {@link android.view.Surface#ROTATION_180},
@@ -301,6 +308,7 @@
&& overscanTop == other.overscanTop
&& overscanRight == other.overscanRight
&& overscanBottom == other.overscanBottom
+ && Objects.equal(displayCutout, other.displayCutout)
&& rotation == other.rotation
&& modeId == other.modeId
&& defaultModeId == other.defaultModeId
@@ -342,6 +350,7 @@
overscanTop = other.overscanTop;
overscanRight = other.overscanRight;
overscanBottom = other.overscanBottom;
+ displayCutout = other.displayCutout;
rotation = other.rotation;
modeId = other.modeId;
defaultModeId = other.defaultModeId;
@@ -379,6 +388,7 @@
overscanTop = source.readInt();
overscanRight = source.readInt();
overscanBottom = source.readInt();
+ displayCutout = DisplayCutout.ParcelableWrapper.readCutoutFromParcel(source);
rotation = source.readInt();
modeId = source.readInt();
defaultModeId = source.readInt();
@@ -425,6 +435,7 @@
dest.writeInt(overscanTop);
dest.writeInt(overscanRight);
dest.writeInt(overscanBottom);
+ DisplayCutout.ParcelableWrapper.writeCutoutToParcel(displayCutout, dest, flags);
dest.writeInt(rotation);
dest.writeInt(modeId);
dest.writeInt(defaultModeId);
diff --git a/android/view/IWindowManagerImpl.java b/android/view/IWindowManagerImpl.java
index 4d804c5..93e6c0b 100644
--- a/android/view/IWindowManagerImpl.java
+++ b/android/view/IWindowManagerImpl.java
@@ -29,6 +29,7 @@
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.DisplayMetrics;
+import android.view.RemoteAnimationAdapter;
import com.android.internal.os.IResultReceiver;
import com.android.internal.policy.IKeyguardDismissCallback;
@@ -76,6 +77,11 @@
// ---- unused implementation of IWindowManager ----
@Override
+ public int getNavBarPosition() throws RemoteException {
+ return 0;
+ }
+
+ @Override
public void addWindowToken(IBinder arg0, int arg1, int arg2) throws RemoteException {
// TODO Auto-generated method stub
@@ -237,6 +243,10 @@
}
@Override
+ public void overridePendingAppTransitionRemote(RemoteAnimationAdapter adapter) {
+ }
+
+ @Override
public void prepareAppTransition(int arg0, boolean arg1) throws RemoteException {
// TODO Auto-generated method stub
@@ -416,7 +426,8 @@
}
@Override
- public void dismissKeyguard(IKeyguardDismissCallback callback) throws RemoteException {
+ public void dismissKeyguard(IKeyguardDismissCallback callback, CharSequence message)
+ throws RemoteException {
}
@Override
@@ -537,4 +548,17 @@
public void unregisterWallpaperVisibilityListener(IWallpaperVisibilityListener listener,
int displayId) throws RemoteException {
}
+
+ @Override
+ public void startWindowTrace() throws RemoteException {
+ }
+
+ @Override
+ public void stopWindowTrace() throws RemoteException {
+ }
+
+ @Override
+ public boolean isWindowTraceEnabled() throws RemoteException {
+ return false;
+ }
}
diff --git a/android/view/KeyEvent.java b/android/view/KeyEvent.java
index a2147b7..a597405 100644
--- a/android/view/KeyEvent.java
+++ b/android/view/KeyEvent.java
@@ -804,11 +804,12 @@
public static final int KEYCODE_SYSTEM_NAVIGATION_LEFT = 282;
/** Key code constant: Consumed by the system for navigation right */
public static final int KEYCODE_SYSTEM_NAVIGATION_RIGHT = 283;
- /** Key code constant: Show all apps
- * @hide */
+ /** Key code constant: Show all apps */
public static final int KEYCODE_ALL_APPS = 284;
+ /** Key code constant: Refresh key. */
+ public static final int KEYCODE_REFRESH = 285;
- private static final int LAST_KEYCODE = KEYCODE_ALL_APPS;
+ private static final int LAST_KEYCODE = KEYCODE_REFRESH;
// NOTE: If you add a new keycode here you must also add it to:
// isSystem()
diff --git a/android/view/MotionEvent.java b/android/view/MotionEvent.java
index 04fa637..1d7c1de 100644
--- a/android/view/MotionEvent.java
+++ b/android/view/MotionEvent.java
@@ -26,6 +26,8 @@
import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;
+import java.util.Objects;
+
/**
* Object used to report movement (mouse, pen, finger, trackball) events.
* Motion events may hold either absolute or relative movements and other data,
@@ -173,6 +175,8 @@
private static final long NS_PER_MS = 1000000;
private static final String LABEL_PREFIX = "AXIS_";
+ private static final boolean DEBUG_CONCISE_TOSTRING = false;
+
/**
* An invalid pointer id.
*
@@ -3236,31 +3240,42 @@
public String toString() {
StringBuilder msg = new StringBuilder();
msg.append("MotionEvent { action=").append(actionToString(getAction()));
- msg.append(", actionButton=").append(buttonStateToString(getActionButton()));
+ appendUnless("0", msg, ", actionButton=", buttonStateToString(getActionButton()));
final int pointerCount = getPointerCount();
for (int i = 0; i < pointerCount; i++) {
- msg.append(", id[").append(i).append("]=").append(getPointerId(i));
- msg.append(", x[").append(i).append("]=").append(getX(i));
- msg.append(", y[").append(i).append("]=").append(getY(i));
- msg.append(", toolType[").append(i).append("]=").append(
- toolTypeToString(getToolType(i)));
+ appendUnless(i, msg, ", id[" + i + "]=", getPointerId(i));
+ float x = getX(i);
+ float y = getY(i);
+ if (!DEBUG_CONCISE_TOSTRING || x != 0f || y != 0f) {
+ msg.append(", x[").append(i).append("]=").append(x);
+ msg.append(", y[").append(i).append("]=").append(y);
+ }
+ appendUnless(TOOL_TYPE_SYMBOLIC_NAMES.get(TOOL_TYPE_FINGER),
+ msg, ", toolType[" + i + "]=", toolTypeToString(getToolType(i)));
}
- msg.append(", buttonState=").append(MotionEvent.buttonStateToString(getButtonState()));
- msg.append(", metaState=").append(KeyEvent.metaStateToString(getMetaState()));
- msg.append(", flags=0x").append(Integer.toHexString(getFlags()));
- msg.append(", edgeFlags=0x").append(Integer.toHexString(getEdgeFlags()));
- msg.append(", pointerCount=").append(pointerCount);
- msg.append(", historySize=").append(getHistorySize());
+ appendUnless("0", msg, ", buttonState=", MotionEvent.buttonStateToString(getButtonState()));
+ appendUnless("0", msg, ", metaState=", KeyEvent.metaStateToString(getMetaState()));
+ appendUnless("0", msg, ", flags=0x", Integer.toHexString(getFlags()));
+ appendUnless("0", msg, ", edgeFlags=0x", Integer.toHexString(getEdgeFlags()));
+ appendUnless(1, msg, ", pointerCount=", pointerCount);
+ appendUnless(0, msg, ", historySize=", getHistorySize());
msg.append(", eventTime=").append(getEventTime());
- msg.append(", downTime=").append(getDownTime());
- msg.append(", deviceId=").append(getDeviceId());
- msg.append(", source=0x").append(Integer.toHexString(getSource()));
+ if (!DEBUG_CONCISE_TOSTRING) {
+ msg.append(", downTime=").append(getDownTime());
+ msg.append(", deviceId=").append(getDeviceId());
+ msg.append(", source=0x").append(Integer.toHexString(getSource()));
+ }
msg.append(" }");
return msg.toString();
}
+ private static <T> void appendUnless(T defValue, StringBuilder sb, String key, T value) {
+ if (DEBUG_CONCISE_TOSTRING && Objects.equals(defValue, value)) return;
+ sb.append(key).append(value);
+ }
+
/**
* Returns a string that represents the symbolic name of the specified unmasked action
* such as "ACTION_DOWN", "ACTION_POINTER_DOWN(3)" or an equivalent numeric constant
diff --git a/android/view/NotificationHeaderView.java b/android/view/NotificationHeaderView.java
index ab0b3ee..fbba8ab 100644
--- a/android/view/NotificationHeaderView.java
+++ b/android/view/NotificationHeaderView.java
@@ -47,6 +47,7 @@
private final int mGravity;
private View mAppName;
private View mHeaderText;
+ private View mSecondaryHeaderText;
private OnClickListener mExpandClickListener;
private HeaderTouchListener mTouchListener = new HeaderTouchListener();
private ImageView mExpandButton;
@@ -58,7 +59,6 @@
private boolean mShowExpandButtonAtEnd;
private boolean mShowWorkBadgeAtEnd;
private Drawable mBackground;
- private int mHeaderBackgroundHeight;
private boolean mEntireHeaderClickable;
private boolean mExpandOnlyOnButton;
private boolean mAcceptAllTouches;
@@ -68,7 +68,7 @@
@Override
public void getOutline(View view, Outline outline) {
if (mBackground != null) {
- outline.setRect(0, 0, getWidth(), mHeaderBackgroundHeight);
+ outline.setRect(0, 0, getWidth(), getHeight());
outline.setAlpha(1f);
}
}
@@ -91,8 +91,6 @@
Resources res = getResources();
mChildMinWidth = res.getDimensionPixelSize(R.dimen.notification_header_shrink_min_width);
mContentEndMargin = res.getDimensionPixelSize(R.dimen.notification_content_margin_end);
- mHeaderBackgroundHeight = res.getDimensionPixelSize(
- R.dimen.notification_header_background_height);
mEntireHeaderClickable = res.getBoolean(R.bool.config_notificationHeaderClickableForExpand);
int[] attrIds = { android.R.attr.gravity };
@@ -106,6 +104,7 @@
super.onFinishInflate();
mAppName = findViewById(com.android.internal.R.id.app_name_text);
mHeaderText = findViewById(com.android.internal.R.id.header_text);
+ mSecondaryHeaderText = findViewById(com.android.internal.R.id.header_text_secondary);
mExpandButton = findViewById(com.android.internal.R.id.expand_button);
mIcon = findViewById(com.android.internal.R.id.icon);
mProfileBadge = findViewById(com.android.internal.R.id.profile_badge);
@@ -137,26 +136,33 @@
if (totalWidth > givenWidth) {
int overFlow = totalWidth - givenWidth;
// We are overflowing, lets shrink the app name first
- final int appWidth = mAppName.getMeasuredWidth();
- if (overFlow > 0 && mAppName.getVisibility() != GONE && appWidth > mChildMinWidth) {
- int newSize = appWidth - Math.min(appWidth - mChildMinWidth, overFlow);
- int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
- mAppName.measure(childWidthSpec, wrapContentHeightSpec);
- overFlow -= appWidth - newSize;
- }
- // still overflowing, finaly we shrink the header text
- if (overFlow > 0 && mHeaderText.getVisibility() != GONE) {
- // we're still too big
- final int textWidth = mHeaderText.getMeasuredWidth();
- int newSize = Math.max(0, textWidth - overFlow);
- int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
- mHeaderText.measure(childWidthSpec, wrapContentHeightSpec);
- }
+ overFlow = shrinkViewForOverflow(wrapContentHeightSpec, overFlow, mAppName,
+ mChildMinWidth);
+
+ // still overflowing, we shrink the header text
+ overFlow = shrinkViewForOverflow(wrapContentHeightSpec, overFlow, mHeaderText, 0);
+
+ // still overflowing, finally we shrink the secondary header text
+ shrinkViewForOverflow(wrapContentHeightSpec, overFlow, mSecondaryHeaderText,
+ 0);
}
mTotalWidth = Math.min(totalWidth, givenWidth);
setMeasuredDimension(givenWidth, givenHeight);
}
+ private int shrinkViewForOverflow(int heightSpec, int overFlow, View targetView,
+ int minimumWidth) {
+ final int oldWidth = targetView.getMeasuredWidth();
+ if (overFlow > 0 && targetView.getVisibility() != GONE && oldWidth > minimumWidth) {
+ // we're still too big
+ int newSize = Math.max(minimumWidth, oldWidth - overFlow);
+ int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
+ targetView.measure(childWidthSpec, heightSpec);
+ overFlow -= oldWidth - newSize;
+ }
+ return overFlow;
+ }
+
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = getPaddingStart();
@@ -228,7 +234,7 @@
@Override
protected void onDraw(Canvas canvas) {
if (mBackground != null) {
- mBackground.setBounds(0, 0, getWidth(), mHeaderBackgroundHeight);
+ mBackground.setBounds(0, 0, getWidth(), getHeight());
mBackground.draw(canvas);
}
}
diff --git a/android/view/PointerIcon.java b/android/view/PointerIcon.java
index 998fd01..3fd4696 100644
--- a/android/view/PointerIcon.java
+++ b/android/view/PointerIcon.java
@@ -461,8 +461,10 @@
+ "refer to a bitmap drawable.");
}
+ final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
+ validateHotSpot(bitmap, hotSpotX, hotSpotY);
// Set the properties now that we have successfully loaded the icon.
- mBitmap = ((BitmapDrawable)drawable).getBitmap();
+ mBitmap = bitmap;
mHotSpotX = hotSpotX;
mHotSpotY = hotSpotY;
}
diff --git a/android/view/RecordingCanvas.java b/android/view/RecordingCanvas.java
index 5088cdc..fbb862b 100644
--- a/android/view/RecordingCanvas.java
+++ b/android/view/RecordingCanvas.java
@@ -34,6 +34,7 @@
import android.graphics.RectF;
import android.graphics.TemporaryBuffer;
import android.text.GraphicsOperations;
+import android.text.MeasuredText;
import android.text.SpannableString;
import android.text.SpannedString;
import android.text.TextUtils;
@@ -473,7 +474,8 @@
}
nDrawTextRun(mNativeCanvasWrapper, text, index, count, contextIndex, contextCount,
- x, y, isRtl, paint.getNativeInstance());
+ x, y, isRtl, paint.getNativeInstance(), 0 /* measured text */,
+ 0 /* measured text offset */);
}
@Override
@@ -503,8 +505,20 @@
int len = end - start;
char[] buf = TemporaryBuffer.obtain(contextLen);
TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
+ long measuredTextPtr = 0;
+ int measuredTextOffset = 0;
+ if (text instanceof MeasuredText) {
+ MeasuredText mt = (MeasuredText) text;
+ int paraIndex = mt.findParaIndex(start);
+ if (end <= mt.getParagraphEnd(paraIndex)) {
+ // Only support if the target is in the same paragraph.
+ measuredTextPtr = mt.getMeasuredParagraph(paraIndex).getNativePtr();
+ measuredTextOffset = start - mt.getParagraphStart(paraIndex);
+ }
+ }
nDrawTextRun(mNativeCanvasWrapper, buf, start - contextStart, len,
- 0, contextLen, x, y, isRtl, paint.getNativeInstance());
+ 0, contextLen, x, y, isRtl, paint.getNativeInstance(),
+ measuredTextPtr, measuredTextOffset);
TemporaryBuffer.recycle(buf);
}
}
@@ -626,7 +640,8 @@
@FastNative
private static native void nDrawTextRun(long nativeCanvas, char[] text, int start, int count,
- int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint);
+ int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint,
+ long nativeMeasuredText, int measuredTextOffset);
@FastNative
private static native void nDrawTextOnPath(long nativeCanvas, char[] text, int index, int count,
diff --git a/android/view/RemoteAnimationAdapter.java b/android/view/RemoteAnimationAdapter.java
new file mode 100644
index 0000000..d597e59
--- /dev/null
+++ b/android/view/RemoteAnimationAdapter.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2018 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.ActivityOptions;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Object that describes how to run a remote animation.
+ * <p>
+ * A remote animation lets another app control the entire app transition. It does so by
+ * <ul>
+ * <li>using {@link ActivityOptions#makeRemoteAnimation}</li>
+ * <li>using {@link IWindowManager#overridePendingAppTransitionRemote}</li>
+ * </ul>
+ * to register a {@link RemoteAnimationAdapter} that describes how the animation should be run:
+ * Along some meta-data, this object contains a callback that gets invoked from window manager when
+ * the transition is ready to be started.
+ * <p>
+ * Window manager supplies a list of {@link RemoteAnimationTarget}s into the callback. Each target
+ * contains information about the activity that is animating as well as
+ * {@link RemoteAnimationTarget#leash}. The controlling app can modify the leash like any other
+ * {@link SurfaceControl}, including the possibility to synchronize updating the leash's surface
+ * properties with a frame to be drawn using
+ * {@link SurfaceControl.Transaction#deferTransactionUntil}.
+ * <p>
+ * When the animation is done, the controlling app can invoke
+ * {@link IRemoteAnimationFinishedCallback} that gets supplied into
+ * {@link IRemoteAnimationRunner#onStartAnimation}
+ *
+ * @hide
+ */
+public class RemoteAnimationAdapter implements Parcelable {
+
+ private final IRemoteAnimationRunner mRunner;
+ private final long mDuration;
+ private final long mStatusBarTransitionDelay;
+
+ /**
+ * @param runner The interface that gets notified when we actually need to start the animation.
+ * @param duration The duration of the animation.
+ * @param statusBarTransitionDelay The desired delay for all visual animations in the
+ * status bar caused by this app animation in millis.
+ */
+ public RemoteAnimationAdapter(IRemoteAnimationRunner runner, long duration,
+ long statusBarTransitionDelay) {
+ mRunner = runner;
+ mDuration = duration;
+ mStatusBarTransitionDelay = statusBarTransitionDelay;
+ }
+
+ public RemoteAnimationAdapter(Parcel in) {
+ mRunner = IRemoteAnimationRunner.Stub.asInterface(in.readStrongBinder());
+ mDuration = in.readLong();
+ mStatusBarTransitionDelay = in.readLong();
+ }
+
+ public IRemoteAnimationRunner getRunner() {
+ return mRunner;
+ }
+
+ public long getDuration() {
+ return mDuration;
+ }
+
+ public long getStatusBarTransitionDelay() {
+ return mStatusBarTransitionDelay;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStrongInterface(mRunner);
+ dest.writeLong(mDuration);
+ dest.writeLong(mStatusBarTransitionDelay);
+ }
+
+ public static final Creator<RemoteAnimationAdapter> CREATOR
+ = new Creator<RemoteAnimationAdapter>() {
+ public RemoteAnimationAdapter createFromParcel(Parcel in) {
+ return new RemoteAnimationAdapter(in);
+ }
+
+ public RemoteAnimationAdapter[] newArray(int size) {
+ return new RemoteAnimationAdapter[size];
+ }
+ };
+}
diff --git a/android/view/RemoteAnimationDefinition.java b/android/view/RemoteAnimationDefinition.java
new file mode 100644
index 0000000..381f692
--- /dev/null
+++ b/android/view/RemoteAnimationDefinition.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2018 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.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.util.SparseArray;
+import android.view.WindowManager.TransitionType;
+
+/**
+ * Defines which animation types should be overridden by which remote animation.
+ *
+ * @hide
+ */
+public class RemoteAnimationDefinition implements Parcelable {
+
+ private final SparseArray<RemoteAnimationAdapter> mTransitionAnimationMap;
+
+ public RemoteAnimationDefinition() {
+ mTransitionAnimationMap = new SparseArray<>();
+ }
+
+ /**
+ * Registers a remote animation for a specific transition.
+ *
+ * @param transition The transition type. Must be one of WindowManager.TRANSIT_* values.
+ * @param adapter The adapter that described how to run the remote animation.
+ */
+ public void addRemoteAnimation(@TransitionType int transition, RemoteAnimationAdapter adapter) {
+ mTransitionAnimationMap.put(transition, adapter);
+ }
+
+ /**
+ * Checks whether a remote animation for specific transition is defined.
+ *
+ * @param transition The transition type. Must be one of WindowManager.TRANSIT_* values.
+ * @return Whether this definition has defined a remote animation for the specified transition.
+ */
+ public boolean hasTransition(@TransitionType int transition) {
+ return mTransitionAnimationMap.get(transition) != null;
+ }
+
+ /**
+ * Retrieves the remote animation for a specific transition.
+ *
+ * @param transition The transition type. Must be one of WindowManager.TRANSIT_* values.
+ * @return The remote animation adapter for the specified transition.
+ */
+ public @Nullable RemoteAnimationAdapter getAdapter(@TransitionType int transition) {
+ return mTransitionAnimationMap.get(transition);
+ }
+
+ public RemoteAnimationDefinition(Parcel in) {
+ mTransitionAnimationMap = in.readSparseArray(null /* loader */);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeSparseArray((SparseArray) mTransitionAnimationMap);
+ }
+
+ public static final Creator<RemoteAnimationDefinition> CREATOR =
+ new Creator<RemoteAnimationDefinition>() {
+ public RemoteAnimationDefinition createFromParcel(Parcel in) {
+ return new RemoteAnimationDefinition(in);
+ }
+
+ public RemoteAnimationDefinition[] newArray(int size) {
+ return new RemoteAnimationDefinition[size];
+ }
+ };
+}
diff --git a/android/view/RemoteAnimationTarget.java b/android/view/RemoteAnimationTarget.java
new file mode 100644
index 0000000..c28c389
--- /dev/null
+++ b/android/view/RemoteAnimationTarget.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2018 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.IntDef;
+import android.app.WindowConfiguration;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Describes an activity to be animated as part of a remote animation.
+ *
+ * @hide
+ */
+public class RemoteAnimationTarget implements Parcelable {
+
+ /**
+ * The app is in the set of opening apps of this transition.
+ */
+ public static final int MODE_OPENING = 0;
+
+ /**
+ * The app is in the set of closing apps of this transition.
+ */
+ public static final int MODE_CLOSING = 1;
+
+ @IntDef(prefix = { "MODE_" }, value = {
+ MODE_OPENING,
+ MODE_CLOSING
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Mode {}
+
+ /**
+ * The {@link Mode} to describe whether this app is opening or closing.
+ */
+ public final @Mode int mode;
+
+ /**
+ * The id of the task this app belongs to.
+ */
+ public final int taskId;
+
+ /**
+ * The {@link SurfaceControl} object to actually control the transform of the app.
+ */
+ public final SurfaceControl leash;
+
+ /**
+ * Whether the app is translucent and may reveal apps behind.
+ */
+ public final boolean isTranslucent;
+
+ /**
+ * The clip rect window manager applies when clipping the app's main surface in screen space
+ * coordinates. This is just a hint to the animation runner: If running a clip-rect animation,
+ * anything that extends beyond these bounds will not have any effect. This implies that any
+ * clip-rect animation should likely stop at these bounds.
+ */
+ public final Rect clipRect;
+
+ /**
+ * The index of the element in the tree in prefix order. This should be used for z-layering
+ * to preserve original z-layer order in the hierarchy tree assuming no "boosting" needs to
+ * happen.
+ */
+ public final int prefixOrderIndex;
+
+ /**
+ * The source position of the app, in screen spaces coordinates. If the position of the leash
+ * is modified from the controlling app, any animation transform needs to be offset by this
+ * amount.
+ */
+ public final Point position;
+
+ /**
+ * The bounds of the source container the app lives in, in screen space coordinates. If the crop
+ * of the leash is modified from the controlling app, it needs to take the source container
+ * bounds into account when calculating the crop.
+ */
+ public final Rect sourceContainerBounds;
+
+ /**
+ * The window configuration for the target.
+ */
+ public final WindowConfiguration windowConfiguration;
+
+ public RemoteAnimationTarget(int taskId, int mode, SurfaceControl leash, boolean isTranslucent,
+ Rect clipRect, int prefixOrderIndex, Point position, Rect sourceContainerBounds,
+ WindowConfiguration windowConfig) {
+ this.mode = mode;
+ this.taskId = taskId;
+ this.leash = leash;
+ this.isTranslucent = isTranslucent;
+ this.clipRect = new Rect(clipRect);
+ this.prefixOrderIndex = prefixOrderIndex;
+ this.position = new Point(position);
+ this.sourceContainerBounds = new Rect(sourceContainerBounds);
+ this.windowConfiguration = windowConfig;
+ }
+
+ public RemoteAnimationTarget(Parcel in) {
+ taskId = in.readInt();
+ mode = in.readInt();
+ leash = in.readParcelable(null);
+ isTranslucent = in.readBoolean();
+ clipRect = in.readParcelable(null);
+ prefixOrderIndex = in.readInt();
+ position = in.readParcelable(null);
+ sourceContainerBounds = in.readParcelable(null);
+ windowConfiguration = in.readParcelable(null);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(taskId);
+ dest.writeInt(mode);
+ dest.writeParcelable(leash, 0 /* flags */);
+ dest.writeBoolean(isTranslucent);
+ dest.writeParcelable(clipRect, 0 /* flags */);
+ dest.writeInt(prefixOrderIndex);
+ dest.writeParcelable(position, 0 /* flags */);
+ dest.writeParcelable(sourceContainerBounds, 0 /* flags */);
+ dest.writeParcelable(windowConfiguration, 0 /* flags */);
+ }
+
+ public static final Creator<RemoteAnimationTarget> CREATOR
+ = new Creator<RemoteAnimationTarget>() {
+ public RemoteAnimationTarget createFromParcel(Parcel in) {
+ return new RemoteAnimationTarget(in);
+ }
+
+ public RemoteAnimationTarget[] newArray(int size) {
+ return new RemoteAnimationTarget[size];
+ }
+ };
+}
diff --git a/android/view/Surface.java b/android/view/Surface.java
index a417a4a..8830c90 100644
--- a/android/view/Surface.java
+++ b/android/view/Surface.java
@@ -182,6 +182,11 @@
* SurfaceTexture}, which can attach them to an OpenGL ES texture via {@link
* SurfaceTexture#updateTexImage}.
*
+ * Please note that holding onto the Surface created here is not enough to
+ * keep the provided SurfaceTexture from being reclaimed. In that sense,
+ * the Surface will act like a
+ * {@link java.lang.ref.WeakReference weak reference} to the SurfaceTexture.
+ *
* @param surfaceTexture The {@link SurfaceTexture} that is updated by this
* Surface.
* @throws OutOfResourcesException if the surface could not be created.
@@ -278,6 +283,7 @@
*/
public long getNextFrameNumber() {
synchronized (mLock) {
+ checkNotReleasedLocked();
return nativeGetNextFrameNumber(mNativeObject);
}
}
diff --git a/android/view/SurfaceControl.java b/android/view/SurfaceControl.java
index 268e460..bd7f8e5 100644
--- a/android/view/SurfaceControl.java
+++ b/android/view/SurfaceControl.java
@@ -16,20 +16,22 @@
package android.view;
-import static android.view.Surface.ROTATION_270;
-import static android.view.Surface.ROTATION_90;
import static android.graphics.Matrix.MSCALE_X;
import static android.graphics.Matrix.MSCALE_Y;
import static android.graphics.Matrix.MSKEW_X;
import static android.graphics.Matrix.MSKEW_Y;
import static android.graphics.Matrix.MTRANS_X;
import static android.graphics.Matrix.MTRANS_Y;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+import static android.view.SurfaceControlProto.HASH_CODE;
+import static android.view.SurfaceControlProto.NAME;
import android.annotation.Size;
import android.graphics.Bitmap;
import android.graphics.GraphicBuffer;
-import android.graphics.PixelFormat;
import android.graphics.Matrix;
+import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
@@ -40,11 +42,13 @@
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Log;
+import android.util.proto.ProtoOutputStream;
import android.view.Surface.OutOfResourcesException;
import com.android.internal.annotations.GuardedBy;
import dalvik.system.CloseGuard;
+
import libcore.util.NativeAllocationRegistry;
import java.io.Closeable;
@@ -628,6 +632,21 @@
nativeWriteToParcel(mNativeObject, dest);
}
+ /**
+ * Write to a protocol buffer output stream. Protocol buffer message definition is at {@link
+ * android.view.SurfaceControlProto}.
+ *
+ * @param proto Stream to write the SurfaceControl object to.
+ * @param fieldId Field Id of the SurfaceControl as defined in the parent message.
+ * @hide
+ */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(HASH_CODE, System.identityHashCode(this));
+ proto.write(NAME, mName);
+ proto.end(token);
+ }
+
public static final Creator<SurfaceControl> CREATOR
= new Creator<SurfaceControl>() {
public SurfaceControl createFromParcel(Parcel in) {
diff --git a/android/view/ThreadedRenderer.java b/android/view/ThreadedRenderer.java
index 6a8f8b1..8b730f2 100644
--- a/android/view/ThreadedRenderer.java
+++ b/android/view/ThreadedRenderer.java
@@ -969,8 +969,6 @@
mInitialized = true;
mAppContext = context.getApplicationContext();
- // b/68769804: For low FPS experiments.
- setFPSDivisor(SystemProperties.getInt(DEBUG_FPS_DIVISOR, 1));
initSched(renderProxy);
initGraphicsStats();
}
@@ -1025,9 +1023,7 @@
/** b/68769804: For low FPS experiments. */
public static void setFPSDivisor(int divisor) {
- if (divisor <= 0) divisor = 1;
- Choreographer.getInstance().setFPSDivisor(divisor);
- nHackySetRTAnimationsEnabled(divisor == 1);
+ nHackySetRTAnimationsEnabled(divisor <= 1);
}
/** Not actually public - internal use only. This doc to make lint happy */
diff --git a/android/view/View.java b/android/view/View.java
index cc63a62..3d6a6fe 100644
--- a/android/view/View.java
+++ b/android/view/View.java
@@ -16,6 +16,8 @@
package android.view;
+import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED;
+
import static java.lang.Math.max;
import android.animation.AnimatorInflater;
@@ -3226,6 +3228,11 @@
*/
private static final int PFLAG3_SCREEN_READER_FOCUSABLE = 0x10000000;
+ /**
+ * The last aggregated visibility. Used to detect when it truly changes.
+ */
+ private static final int PFLAG3_AGGREGATED_VISIBLE = 0x20000000;
+
/* End of masks for mPrivateFlags3 */
/**
@@ -3387,6 +3394,18 @@
* decorations when they are shown. You can perform layout of your inner
* UI elements to account for non-fullscreen system UI through the
* {@link #fitSystemWindows(Rect)} method.
+ *
+ * <p>Note: on displays that have a {@link DisplayCutout}, the window may still be placed
+ * differently than if {@link #SYSTEM_UI_FLAG_FULLSCREEN} was set, if the
+ * window's {@link WindowManager.LayoutParams#layoutInDisplayCutoutMode
+ * layoutInDisplayCutoutMode} is
+ * {@link WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
+ * LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT}. To avoid this, use either of the other modes.
+ *
+ * @see WindowManager.LayoutParams#layoutInDisplayCutoutMode
+ * @see WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
+ * @see WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+ * @see WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
*/
public static final int SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN = 0x00000400;
@@ -4045,6 +4064,12 @@
private CharSequence mContentDescription;
/**
+ * If this view represents a distinct part of the window, it can have a title that labels the
+ * area.
+ */
+ private CharSequence mAccessibilityPaneTitle;
+
+ /**
* Specifies the id of a view for which this view serves as a label for
* accessibility purposes.
*/
@@ -4182,6 +4207,16 @@
*/
private static boolean sUseDefaultFocusHighlight;
+ /**
+ * True if zero-sized views can be focused.
+ */
+ private static boolean sCanFocusZeroSized;
+
+ /**
+ * Always assign focus if a focusable View is available.
+ */
+ private static boolean sAlwaysAssignFocus;
+
private String mTransitionName;
static class TintInfo {
@@ -4407,7 +4442,6 @@
private CheckForLongPress mPendingCheckForLongPress;
private CheckForTap mPendingCheckForTap = null;
private PerformClick mPerformClick;
- private SendViewScrolledAccessibilityEvent mSendViewScrolledAccessibilityEvent;
private UnsetPressedState mUnsetPressedState;
@@ -4798,6 +4832,10 @@
sThrowOnInvalidFloatProperties = targetSdkVersion >= Build.VERSION_CODES.P;
+ sCanFocusZeroSized = targetSdkVersion < Build.VERSION_CODES.P;
+
+ sAlwaysAssignFocus = targetSdkVersion < Build.VERSION_CODES.P;
+
sCompatibilityDone = true;
}
}
@@ -5402,6 +5440,11 @@
setScreenReaderFocusable(a.getBoolean(attr, false));
}
break;
+ case R.styleable.View_accessibilityPaneTitle:
+ if (a.peekValue(attr) != null) {
+ setAccessibilityPaneTitle(a.getString(attr));
+ }
+ break;
}
}
@@ -6983,8 +7026,8 @@
* Called when this view wants to give up focus. If focus is cleared
* {@link #onFocusChanged(boolean, int, android.graphics.Rect)} is called.
* <p>
- * <strong>Note:</strong> When a View clears focus the framework is trying
- * to give focus to the first focusable View from the top. Hence, if this
+ * <strong>Note:</strong> When not in touch-mode, the framework will try to give focus
+ * to the first focusable View from the top after focus is cleared. Hence, if this
* View is the first from the top that can take focus, then all callbacks
* related to clearing focus will be invoked after which the framework will
* give focus to this view.
@@ -6995,7 +7038,8 @@
System.out.println(this + " clearFocus()");
}
- clearFocusInternal(null, true, true);
+ final boolean refocus = sAlwaysAssignFocus || !isInTouchMode();
+ clearFocusInternal(null, true, refocus);
}
/**
@@ -7010,6 +7054,7 @@
void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
mPrivateFlags &= ~PFLAG_FOCUSED;
+ clearParentsWantFocus();
if (propagate && mParent != null) {
mParent.clearChildFocus(this);
@@ -7156,7 +7201,7 @@
if (gainFocus) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
} else {
- notifyViewAccessibilityStateChangedIfNeeded(
+ notifyAccessibilityStateChanged(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
}
@@ -7189,20 +7234,24 @@
notifyEnterOrExitForAutoFillIfNeeded(gainFocus);
}
- private void notifyEnterOrExitForAutoFillIfNeeded(boolean enter) {
- if (isAutofillable() && isAttachedToWindow()) {
+ /** @hide */
+ public void notifyEnterOrExitForAutoFillIfNeeded(boolean enter) {
+ if (canNotifyAutofillEnterExitEvent()) {
AutofillManager afm = getAutofillManager();
if (afm != null) {
- if (enter && hasWindowFocus() && isFocused()) {
+ if (enter && isFocused()) {
// We have not been laid out yet, hence cannot evaluate
// whether this view is visible to the user, we will do
// the evaluation once layout is complete.
if (!isLaidOut()) {
mPrivateFlags3 |= PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
} else if (isVisibleToUser()) {
+ // TODO This is a potential problem that View gets focus before it's visible
+ // to User. Ideally View should handle the event when isVisibleToUser()
+ // becomes true where it should issue notifyViewEntered().
afm.notifyViewEntered(this);
}
- } else if (!hasWindowFocus() || !isFocused()) {
+ } else if (!isFocused()) {
afm.notifyViewExited(this);
}
}
@@ -7210,6 +7259,34 @@
}
/**
+ * If this view is a visually distinct portion of a window, for example the content view of
+ * a fragment that is replaced, it is considered a pane for accessibility purposes. In order
+ * for accessibility services to understand the views role, and to announce its title as
+ * appropriate, such views should have pane titles.
+ *
+ * @param accessibilityPaneTitle The pane's title.
+ *
+ * {@see AccessibilityNodeInfo#setPaneTitle(CharSequence)}
+ */
+ public void setAccessibilityPaneTitle(CharSequence accessibilityPaneTitle) {
+ if (!TextUtils.equals(accessibilityPaneTitle, mAccessibilityPaneTitle)) {
+ mAccessibilityPaneTitle = accessibilityPaneTitle;
+ notifyAccessibilityStateChanged(AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_TITLE);
+ }
+ }
+
+ /**
+ * Get the title of the pane for purposes of accessibility.
+ *
+ * @return The current pane title.
+ *
+ * {@see #setAccessibilityPaneTitle}.
+ */
+ public CharSequence getAccessibilityPaneTitle() {
+ return mAccessibilityPaneTitle;
+ }
+
+ /**
* Sends an accessibility event of the given type. If accessibility is
* not enabled this method has no effect. The default implementation calls
* {@link #onInitializeAccessibilityEvent(AccessibilityEvent)} first
@@ -7308,7 +7385,12 @@
* @hide
*/
public void sendAccessibilityEventUncheckedInternal(AccessibilityEvent event) {
- if (!isShown()) {
+ // Panes disappearing are relevant even if though the view is no longer visible.
+ boolean isWindowStateChanged =
+ (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ boolean isWindowDisappearedEvent = isWindowStateChanged && ((event.getContentChangeTypes()
+ & AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED) != 0);
+ if (!isShown() && !isWindowDisappearedEvent) {
return;
}
onInitializeAccessibilityEvent(event);
@@ -7416,6 +7498,10 @@
* @hide
*/
public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ if ((event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
+ && !TextUtils.isEmpty(getAccessibilityPaneTitle())) {
+ event.getText().add(getAccessibilityPaneTitle());
+ }
}
/**
@@ -8237,6 +8323,11 @@
&& getAutofillViewId() > LAST_APP_AUTOFILL_ID;
}
+ /** @hide */
+ public boolean canNotifyAutofillEnterExitEvent() {
+ return isAutofillable() && isAttachedToWindow();
+ }
+
private void populateVirtualStructure(ViewStructure structure,
AccessibilityNodeProvider provider, AccessibilityNodeInfo info) {
structure.setId(AccessibilityNodeInfo.getVirtualDescendantId(info.getSourceNodeId()),
@@ -8459,6 +8550,12 @@
info.setLongClickable(isLongClickable());
info.setContextClickable(isContextClickable());
info.setLiveRegion(getAccessibilityLiveRegion());
+ if ((mTooltipInfo != null) && (mTooltipInfo.mTooltipText != null)) {
+ info.setTooltipText(mTooltipInfo.mTooltipText);
+ info.addAction((mTooltipInfo.mTooltipPopup == null)
+ ? AccessibilityNodeInfo.AccessibilityAction.ACTION_SHOW_TOOLTIP
+ : AccessibilityNodeInfo.AccessibilityAction.ACTION_HIDE_TOOLTIP);
+ }
// TODO: These make sense only if we are in an AdapterView but all
// views can be selected. Maybe from accessibility perspective
@@ -8506,6 +8603,7 @@
info.addAction(AccessibilityAction.ACTION_SHOW_ON_SCREEN);
populateAccessibilityNodeInfoDrawingOrderInParent(info);
+ info.setPaneTitle(mAccessibilityPaneTitle);
}
/**
@@ -8826,9 +8924,9 @@
final boolean nonEmptyDesc = contentDescription != null && contentDescription.length() > 0;
if (nonEmptyDesc && getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
} else {
- notifyViewAccessibilityStateChangedIfNeeded(
+ notifyAccessibilityStateChanged(
AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION);
}
}
@@ -8861,8 +8959,7 @@
return;
}
mAccessibilityTraversalBeforeId = beforeId;
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
/**
@@ -8905,8 +9002,7 @@
return;
}
mAccessibilityTraversalAfterId = afterId;
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
/**
@@ -8948,8 +9044,7 @@
&& mID == View.NO_ID) {
mID = generateViewId();
}
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
/**
@@ -10046,6 +10141,13 @@
}
/**
+ * @return {@code true} if laid-out and not about to do another layout.
+ */
+ boolean isLayoutValid() {
+ return isLaidOut() && ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == 0);
+ }
+
+ /**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
@@ -10442,8 +10544,7 @@
if (pflags3 != mPrivateFlags3) {
mPrivateFlags3 = pflags3;
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
}
@@ -10817,7 +10918,7 @@
if (views == null) {
return;
}
- if (!isFocusable() || !isEnabled()) {
+ if (!canTakeFocus()) {
return;
}
if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE
@@ -11031,8 +11132,9 @@
* descendants.
*
* A view will not actually take focus if it is not focusable ({@link #isFocusable} returns
- * false), or if it is focusable and it is not focusable in touch mode
- * ({@link #isFocusableInTouchMode}) while the device is in touch mode.
+ * false), or if it can't be focused due to other conditions (not focusable in touch mode
+ * ({@link #isFocusableInTouchMode}) while the device is in touch mode, not visible, not
+ * enabled, or has no size).
*
* See also {@link #focusSearch(int)}, which is what you call to say that you
* have focus, and you want your parent to look for the next one.
@@ -11139,9 +11241,7 @@
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
// need to be focusable
- if ((mViewFlags & FOCUSABLE) != FOCUSABLE
- || (mViewFlags & VISIBILITY_MASK) != VISIBLE
- || (mViewFlags & ENABLED_MASK) != ENABLED) {
+ if (!canTakeFocus()) {
return false;
}
@@ -11156,10 +11256,23 @@
return false;
}
+ if (!isLayoutValid()) {
+ mPrivateFlags |= PFLAG_WANTS_FOCUS;
+ } else {
+ clearParentsWantFocus();
+ }
+
handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}
+ void clearParentsWantFocus() {
+ if (mParent instanceof View) {
+ ((View) mParent).mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
+ ((View) mParent).clearParentsWantFocus();
+ }
+ }
+
/**
* Call this to try to give focus to a specific view or to one of its descendants. This is a
* special variant of {@link #requestFocus() } that will allow views that are not focusable in
@@ -11261,8 +11374,7 @@
mPrivateFlags2 &= ~PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK;
mPrivateFlags2 |= (mode << PFLAG2_ACCESSIBILITY_LIVE_REGION_SHIFT)
& PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK;
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
}
@@ -11319,10 +11431,9 @@
mPrivateFlags2 |= (mode << PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT)
& PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK;
if (!maySkipNotify || oldIncludeForAccessibility != includeForAccessibility()) {
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
} else {
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
}
}
@@ -11380,6 +11491,7 @@
* {@link #getAccessibilityLiveRegion()} is not
* {@link #ACCESSIBILITY_LIVE_REGION_NONE}.
* </ul>
+ * <li>Has an accessibility pane title, see {@link #setAccessibilityPaneTitle}</li>
* </ol>
*
* @return Whether the view is exposed for accessibility.
@@ -11406,7 +11518,8 @@
return mode == IMPORTANT_FOR_ACCESSIBILITY_YES || isActionableForAccessibility()
|| hasListenersForAccessibility() || getAccessibilityNodeProvider() != null
- || getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE;
+ || getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE
+ || (mAccessibilityPaneTitle != null);
}
/**
@@ -11496,25 +11609,8 @@
*
* @hide
*/
- public void notifyViewAccessibilityStateChangedIfNeeded(int changeType) {
- if (!AccessibilityManager.getInstance(mContext).isEnabled() || mAttachInfo == null) {
- return;
- }
- // If this is a live region, we should send a subtree change event
- // from this view immediately. Otherwise, we can let it propagate up.
- if (getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE) {
- final AccessibilityEvent event = AccessibilityEvent.obtain();
- event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
- event.setContentChangeTypes(changeType);
- sendAccessibilityEventUnchecked(event);
- } else if (mParent != null) {
- try {
- mParent.notifySubtreeAccessibilityStateChanged(this, this, changeType);
- } catch (AbstractMethodError e) {
- Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
- " does not fully implement ViewParent", e);
- }
- }
+ public void notifyAccessibilityStateChanged(int changeType) {
+ notifyAccessibilityStateChanged(this, changeType);
}
/**
@@ -11528,22 +11624,42 @@
*
* @hide
*/
- public void notifySubtreeAccessibilityStateChangedIfNeeded() {
+ public void notifyAccessibilitySubtreeChanged() {
+ if ((mPrivateFlags2 & PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED) == 0) {
+ mPrivateFlags2 |= PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED;
+ notifyAccessibilityStateChanged(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+ }
+ }
+
+ void notifyAccessibilityStateChanged(View source, int changeType) {
if (!AccessibilityManager.getInstance(mContext).isEnabled() || mAttachInfo == null) {
return;
}
- if ((mPrivateFlags2 & PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED) == 0) {
- mPrivateFlags2 |= PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED;
+ // Changes to views with a pane title count as window state changes, as the pane title
+ // marks them as significant parts of the UI.
+ if (!TextUtils.isEmpty(getAccessibilityPaneTitle())) {
+ final AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.setEventType(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ event.setContentChangeTypes(changeType);
+ onPopulateAccessibilityEvent(event);
if (mParent != null) {
try {
- mParent.notifySubtreeAccessibilityStateChanged(
- this, this, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+ mParent.requestSendAccessibilityEvent(this, event);
} catch (AbstractMethodError e) {
- Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
- " does not fully implement ViewParent", e);
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName()
+ + " does not fully implement ViewParent", e);
}
}
}
+
+ if (mParent != null) {
+ try {
+ mParent.notifySubtreeAccessibilityStateChanged(this, source, changeType);
+ } catch (AbstractMethodError e) {
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName()
+ + " does not fully implement ViewParent", e);
+ }
+ }
}
/**
@@ -11563,8 +11679,10 @@
/**
* Reset the flag indicating the accessibility state of the subtree rooted
* at this view changed.
+ *
+ * @hide
*/
- void resetSubtreeAccessibilityStateChanged() {
+ public void resetSubtreeAccessibilityStateChanged() {
mPrivateFlags2 &= ~PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED;
}
@@ -11725,8 +11843,7 @@
|| getAccessibilitySelectionEnd() != end)
&& (start == end)) {
setAccessibilitySelection(start, end);
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
return true;
}
} break;
@@ -11743,6 +11860,21 @@
return true;
}
} break;
+ case R.id.accessibilityActionShowTooltip: {
+ if ((mTooltipInfo != null) && (mTooltipInfo.mTooltipPopup != null)) {
+ // Tooltip already showing
+ return false;
+ }
+ return showLongClickTooltip(0, 0);
+ }
+ case R.id.accessibilityActionHideTooltip: {
+ if ((mTooltipInfo == null) || (mTooltipInfo.mTooltipPopup == null)) {
+ // No tooltip showing
+ return false;
+ }
+ hideTooltip();
+ return true;
+ }
}
return false;
}
@@ -12347,8 +12479,6 @@
imm.focusIn(this);
}
- notifyEnterOrExitForAutoFillIfNeeded(hasWindowFocus);
-
refreshDrawableState();
}
@@ -12467,6 +12597,10 @@
*/
@CallSuper
public void onVisibilityAggregated(boolean isVisible) {
+ // Update our internal visibility tracking so we can detect changes
+ boolean oldVisible = (mPrivateFlags3 & PFLAG3_AGGREGATED_VISIBLE) != 0;
+ mPrivateFlags3 = isVisible ? (mPrivateFlags3 | PFLAG3_AGGREGATED_VISIBLE)
+ : (mPrivateFlags3 & ~PFLAG3_AGGREGATED_VISIBLE);
if (isVisible && mAttachInfo != null) {
initialAwakenScrollBars();
}
@@ -12507,6 +12641,13 @@
}
}
}
+ if (!TextUtils.isEmpty(getAccessibilityPaneTitle())) {
+ if (isVisible != oldVisible) {
+ notifyAccessibilityStateChanged(isVisible
+ ? AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_APPEARED
+ : AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED);
+ }
+ }
}
/**
@@ -13531,6 +13672,13 @@
mAttachInfo.mUnbufferedDispatchRequested = true;
}
+ private boolean canTakeFocus() {
+ return ((mViewFlags & VISIBILITY_MASK) == VISIBLE)
+ && ((mViewFlags & FOCUSABLE) == FOCUSABLE)
+ && ((mViewFlags & ENABLED_MASK) == ENABLED)
+ && (sCanFocusZeroSized || !isLayoutValid() || (mBottom > mTop) && (mRight > mLeft));
+ }
+
/**
* Set flags controlling behavior of this view.
*
@@ -13550,6 +13698,7 @@
return;
}
int privateFlags = mPrivateFlags;
+ boolean shouldNotifyFocusableAvailable = false;
// If focusable is auto, update the FOCUSABLE bit.
int focusableChangedByAuto = 0;
@@ -13588,7 +13737,7 @@
|| focusableChangedByAuto == 0
|| viewRootImpl == null
|| viewRootImpl.mThread == Thread.currentThread()) {
- mParent.focusableViewAvailable(this);
+ shouldNotifyFocusableAvailable = true;
}
}
}
@@ -13611,10 +13760,7 @@
// about in case nothing has focus. even if this specific view
// isn't focusable, it may contain something that is, so let
// the root view try to give this focus if nothing else does.
- if ((mParent != null) && ((mViewFlags & ENABLED_MASK) == ENABLED)
- && (mBottom > mTop) && (mRight > mLeft)) {
- mParent.focusableViewAvailable(this);
- }
+ shouldNotifyFocusableAvailable = true;
}
}
@@ -13623,17 +13769,18 @@
// a view becoming enabled should notify the parent as long as the view is also
// visible and the parent wasn't already notified by becoming visible during this
// setFlags invocation.
- if ((mViewFlags & VISIBILITY_MASK) == VISIBLE
- && ((changed & VISIBILITY_MASK) == 0)) {
- if ((mParent != null) && (mViewFlags & ENABLED_MASK) == ENABLED) {
- mParent.focusableViewAvailable(this);
- }
- }
+ shouldNotifyFocusableAvailable = true;
} else {
if (hasFocus()) clearFocus();
}
}
+ if (shouldNotifyFocusableAvailable) {
+ if (mParent != null && canTakeFocus()) {
+ mParent.focusableViewAvailable(this);
+ }
+ }
+
/* Check if the GONE bit has changed */
if ((changed & GONE) != 0) {
needGlobalAttributesUpdate(false);
@@ -13713,7 +13860,7 @@
((!(mParent instanceof ViewGroup)) || ((ViewGroup) mParent).isShown())) {
dispatchVisibilityAggregated(newVisibility == VISIBLE);
}
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -13759,14 +13906,12 @@
|| (changed & CLICKABLE) != 0 || (changed & LONG_CLICKABLE) != 0
|| (changed & CONTEXT_CLICKABLE) != 0) {
if (oldIncludeForAccessibility != includeForAccessibility()) {
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
} else {
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
} else if ((changed & ENABLED_MASK) != 0) {
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
}
}
@@ -13800,10 +13945,13 @@
* @param oldt Previous vertical scroll origin.
*/
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
- if (AccessibilityManager.getInstance(mContext).isEnabled()) {
- postSendViewScrolledAccessibilityEventCallback(l - oldl, t - oldt);
+ ViewRootImpl root = getViewRootImpl();
+ if (root != null) {
+ root.getAccessibilityState()
+ .getSendViewScrolledAccessibilityEvent()
+ .post(this, /* dx */ l - oldl, /* dy */ t - oldt);
}
mBackgroundSizeChanged = true;
@@ -14199,7 +14347,7 @@
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -14243,7 +14391,7 @@
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -14287,7 +14435,7 @@
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -14324,7 +14472,7 @@
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -14361,7 +14509,7 @@
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -14564,7 +14712,7 @@
if (mTransformationInfo.mAlpha != alpha) {
// Report visibility changes, which can affect children, to accessibility
if ((alpha == 0) ^ (mTransformationInfo.mAlpha == 0)) {
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
mTransformationInfo.mAlpha = alpha;
if (onSetAlpha((int) (alpha * 255))) {
@@ -15066,7 +15214,7 @@
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -15100,7 +15248,7 @@
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -15270,7 +15418,7 @@
public void invalidateOutline() {
rebuildOutline();
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
invalidateViewProperty(false, false);
}
@@ -15465,7 +15613,7 @@
}
invalidateParentIfNeeded();
}
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -15513,7 +15661,7 @@
}
invalidateParentIfNeeded();
}
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -16391,18 +16539,6 @@
}
/**
- * Post a callback to send a {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event.
- * This event is sent at most once every
- * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()}.
- */
- private void postSendViewScrolledAccessibilityEventCallback(int dx, int dy) {
- if (mSendViewScrolledAccessibilityEvent == null) {
- mSendViewScrolledAccessibilityEvent = new SendViewScrolledAccessibilityEvent();
- }
- mSendViewScrolledAccessibilityEvent.post(dx, dy);
- }
-
- /**
* Called by a parent to request that a child update its values for mScrollX
* and mScrollY if necessary. This will typically be done if the child is
* animating a scroll using a {@link android.widget.Scroller Scroller}
@@ -17657,7 +17793,13 @@
removeUnsetPressCallback();
removeLongPressCallback();
removePerformClickCallback();
- cancel(mSendViewScrolledAccessibilityEvent);
+ if (mAttachInfo != null
+ && mAttachInfo.mViewRootImpl.mAccessibilityState != null
+ && mAttachInfo.mViewRootImpl.mAccessibilityState.isScrollEventSenderInitialized()) {
+ mAttachInfo.mViewRootImpl.mAccessibilityState
+ .getSendViewScrolledAccessibilityEvent()
+ .cancelIfPendingFor(this);
+ }
stopNestedScroll();
// Anything that started animating right before detach should already
@@ -17761,6 +17903,15 @@
}
/**
+ * Return the window this view is currently attached to. Used in
+ * {@link android.app.ActivityView} to communicate with WM.
+ * @hide
+ */
+ protected IWindow getWindow() {
+ return mAttachInfo != null ? mAttachInfo.mWindow : null;
+ }
+
+ /**
* Return the visibility value of the least visible component passed.
*/
int combineVisibility(int vis1, int vis2) {
@@ -18936,7 +19087,7 @@
*
* @hide
*/
- public Bitmap createSnapshot(Bitmap.Config quality, int backgroundColor, boolean skipChildren) {
+ public Bitmap createSnapshot(ViewDebug.CanvasProvider canvasProvider, boolean skipChildren) {
int width = mRight - mLeft;
int height = mBottom - mTop;
@@ -18945,71 +19096,48 @@
width = (int) ((width * scale) + 0.5f);
height = (int) ((height * scale) + 0.5f);
- Bitmap bitmap = Bitmap.createBitmap(mResources.getDisplayMetrics(),
- width > 0 ? width : 1, height > 0 ? height : 1, quality);
- if (bitmap == null) {
- throw new OutOfMemoryError();
- }
+ Canvas oldCanvas = null;
+ try {
+ Canvas canvas = canvasProvider.getCanvas(this,
+ width > 0 ? width : 1, height > 0 ? height : 1);
- Resources resources = getResources();
- if (resources != null) {
- bitmap.setDensity(resources.getDisplayMetrics().densityDpi);
- }
-
- Canvas canvas;
- if (attachInfo != null) {
- canvas = attachInfo.mCanvas;
- if (canvas == null) {
- canvas = new Canvas();
+ if (attachInfo != null) {
+ oldCanvas = attachInfo.mCanvas;
+ // Temporarily clobber the cached Canvas in case one of our children
+ // is also using a drawing cache. Without this, the children would
+ // steal the canvas by attaching their own bitmap to it and bad, bad
+ // things would happen (invisible views, corrupted drawings, etc.)
+ attachInfo.mCanvas = null;
}
- canvas.setBitmap(bitmap);
- // Temporarily clobber the cached Canvas in case one of our children
- // is also using a drawing cache. Without this, the children would
- // steal the canvas by attaching their own bitmap to it and bad, bad
- // things would happen (invisible views, corrupted drawings, etc.)
- attachInfo.mCanvas = null;
- } else {
- // This case should hopefully never or seldom happen
- canvas = new Canvas(bitmap);
- }
- boolean enabledHwBitmapsInSwMode = canvas.isHwBitmapsInSwModeEnabled();
- canvas.setHwBitmapsInSwModeEnabled(true);
- if ((backgroundColor & 0xff000000) != 0) {
- bitmap.eraseColor(backgroundColor);
- }
- computeScroll();
- final int restoreCount = canvas.save();
- canvas.scale(scale, scale);
- canvas.translate(-mScrollX, -mScrollY);
+ computeScroll();
+ final int restoreCount = canvas.save();
+ canvas.scale(scale, scale);
+ canvas.translate(-mScrollX, -mScrollY);
- // Temporarily remove the dirty mask
- int flags = mPrivateFlags;
- mPrivateFlags &= ~PFLAG_DIRTY_MASK;
+ // Temporarily remove the dirty mask
+ int flags = mPrivateFlags;
+ mPrivateFlags &= ~PFLAG_DIRTY_MASK;
- // Fast path for layouts with no backgrounds
- if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
- dispatchDraw(canvas);
- drawAutofilledHighlight(canvas);
- if (mOverlay != null && !mOverlay.isEmpty()) {
- mOverlay.getOverlayView().draw(canvas);
+ // Fast path for layouts with no backgrounds
+ if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
+ dispatchDraw(canvas);
+ drawAutofilledHighlight(canvas);
+ if (mOverlay != null && !mOverlay.isEmpty()) {
+ mOverlay.getOverlayView().draw(canvas);
+ }
+ } else {
+ draw(canvas);
}
- } else {
- draw(canvas);
+
+ mPrivateFlags = flags;
+ canvas.restoreToCount(restoreCount);
+ return canvasProvider.createBitmap();
+ } finally {
+ if (oldCanvas != null) {
+ attachInfo.mCanvas = oldCanvas;
+ }
}
-
- mPrivateFlags = flags;
-
- canvas.restoreToCount(restoreCount);
- canvas.setBitmap(null);
- canvas.setHwBitmapsInSwModeEnabled(enabledHwBitmapsInSwMode);
-
- if (attachInfo != null) {
- // Restore the cached Canvas for our siblings
- attachInfo.mCanvas = canvas;
- }
-
- return bitmap;
}
/**
@@ -20160,15 +20288,58 @@
}
}
+ final boolean wasLayoutValid = isLayoutValid();
+
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
+ if (!wasLayoutValid && isFocused()) {
+ mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
+ if (canTakeFocus()) {
+ // We have a robust focus, so parents should no longer be wanting focus.
+ clearParentsWantFocus();
+ } else if (!getViewRootImpl().isInLayout()) {
+ // This is a weird case. Most-likely the user, rather than ViewRootImpl, called
+ // layout. In this case, there's no guarantee that parent layouts will be evaluated
+ // and thus the safest action is to clear focus here.
+ clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
+ clearParentsWantFocus();
+ } else if (!hasParentWantsFocus()) {
+ // original requestFocus was likely on this view directly, so just clear focus
+ clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
+ }
+ // otherwise, we let parents handle re-assigning focus during their layout passes.
+ } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
+ mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
+ View focused = findFocus();
+ if (focused != null) {
+ // Try to restore focus as close as possible to our starting focus.
+ if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
+ // Give up and clear focus once we've reached the top-most parent which wants
+ // focus.
+ focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
+ }
+ }
+ }
+
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
+ private boolean hasParentWantsFocus() {
+ ViewParent parent = mParent;
+ while (parent instanceof ViewGroup) {
+ ViewGroup pv = (ViewGroup) parent;
+ if ((pv.mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
+ return true;
+ }
+ parent = pv.mParent;
+ }
+ return false;
+ }
+
/**
* Called from layout when this view should
* assign a size and position to each of its children.
@@ -20256,7 +20427,7 @@
mForegroundInfo.mBoundsChanged = true;
}
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
return changed;
}
@@ -20275,6 +20446,23 @@
mOverlay.getOverlayView().setRight(newWidth);
mOverlay.getOverlayView().setBottom(newHeight);
}
+ // If this isn't laid out yet, focus assignment will be handled during the "deferment/
+ // backtracking" of requestFocus during layout, so don't touch focus here.
+ if (!sCanFocusZeroSized && isLayoutValid()) {
+ if (newWidth <= 0 || newHeight <= 0) {
+ if (hasFocus()) {
+ clearFocus();
+ if (mParent instanceof ViewGroup) {
+ ((ViewGroup) mParent).clearFocusedInCluster();
+ }
+ }
+ clearAccessibilityFocus();
+ } else if (oldWidth <= 0 || oldHeight <= 0) {
+ if (mParent != null && canTakeFocus()) {
+ mParent.focusableViewAvailable(this);
+ }
+ }
+ }
rebuildOutline();
}
@@ -21683,8 +21871,7 @@
if (selected) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
} else {
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
}
}
@@ -22038,7 +22225,7 @@
*
* @param id the ID to search for
* @return a view with given ID if found, or {@code null} otherwise
- * @see View#findViewById(int)
+ * @see View#requireViewById(int)
*/
@Nullable
public final <T extends View> T findViewById(@IdRes int id) {
@@ -22049,6 +22236,29 @@
}
/**
+ * Finds the first descendant view with the given ID, the view itself if the ID matches
+ * {@link #getId()}, or throws an IllegalArgumentException if the ID is invalid or there is no
+ * matching view in the hierarchy.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see View#findViewById(int)
+ */
+ @NonNull
+ public final <T extends View> T requireViewById(@IdRes int id) {
+ T view = findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException("ID does not reference a View inside this View");
+ }
+ return view;
+ }
+
+ /**
* Finds a view by its unuque and stable accessibility id.
*
* @param accessibilityId The searched accessibility id.
@@ -23391,15 +23601,13 @@
data.prepareToLeaveProcess((flags & View.DRAG_FLAG_GLOBAL) != 0);
}
- boolean okay = false;
-
Point shadowSize = new Point();
Point shadowTouchPoint = new Point();
shadowBuilder.onProvideShadowMetrics(shadowSize, shadowTouchPoint);
- if ((shadowSize.x < 0) || (shadowSize.y < 0) ||
- (shadowTouchPoint.x < 0) || (shadowTouchPoint.y < 0)) {
- throw new IllegalStateException("Drag shadow dimensions must not be negative");
+ if ((shadowSize.x <= 0) || (shadowSize.y <= 0)
+ || (shadowTouchPoint.x < 0) || (shadowTouchPoint.y < 0)) {
+ throw new IllegalStateException("Drag shadow dimensions must be positive");
}
if (ViewDebug.DEBUG_DRAG) {
@@ -23410,40 +23618,50 @@
mAttachInfo.mDragSurface.release();
}
mAttachInfo.mDragSurface = new Surface();
+ mAttachInfo.mDragToken = null;
+
+ final ViewRootImpl root = mAttachInfo.mViewRootImpl;
+ final SurfaceSession session = new SurfaceSession(root.mSurface);
+ final SurfaceControl surface = new SurfaceControl.Builder(session)
+ .setName("drag surface")
+ .setSize(shadowSize.x, shadowSize.y)
+ .setFormat(PixelFormat.TRANSLUCENT)
+ .build();
try {
- mAttachInfo.mDragToken = mAttachInfo.mSession.prepareDrag(mAttachInfo.mWindow,
- flags, shadowSize.x, shadowSize.y, mAttachInfo.mDragSurface);
- if (ViewDebug.DEBUG_DRAG) Log.d(VIEW_LOG_TAG, "prepareDrag returned token="
- + mAttachInfo.mDragToken + " surface=" + mAttachInfo.mDragSurface);
- if (mAttachInfo.mDragToken != null) {
- Canvas canvas = mAttachInfo.mDragSurface.lockCanvas(null);
- try {
- canvas.drawColor(0, PorterDuff.Mode.CLEAR);
- shadowBuilder.onDrawShadow(canvas);
- } finally {
- mAttachInfo.mDragSurface.unlockCanvasAndPost(canvas);
- }
-
- final ViewRootImpl root = getViewRootImpl();
-
- // Cache the local state object for delivery with DragEvents
- root.setLocalDragState(myLocalState);
-
- // repurpose 'shadowSize' for the last touch point
- root.getLastTouchPoint(shadowSize);
-
- okay = mAttachInfo.mSession.performDrag(mAttachInfo.mWindow, mAttachInfo.mDragToken,
- root.getLastTouchSource(), shadowSize.x, shadowSize.y,
- shadowTouchPoint.x, shadowTouchPoint.y, data);
- if (ViewDebug.DEBUG_DRAG) Log.d(VIEW_LOG_TAG, "performDrag returned " + okay);
+ mAttachInfo.mDragSurface.copyFrom(surface);
+ final Canvas canvas = mAttachInfo.mDragSurface.lockCanvas(null);
+ try {
+ canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+ shadowBuilder.onDrawShadow(canvas);
+ } finally {
+ mAttachInfo.mDragSurface.unlockCanvasAndPost(canvas);
}
+
+ // Cache the local state object for delivery with DragEvents
+ root.setLocalDragState(myLocalState);
+
+ // repurpose 'shadowSize' for the last touch point
+ root.getLastTouchPoint(shadowSize);
+
+ mAttachInfo.mDragToken = mAttachInfo.mSession.performDrag(
+ mAttachInfo.mWindow, flags, surface, root.getLastTouchSource(),
+ shadowSize.x, shadowSize.y, shadowTouchPoint.x, shadowTouchPoint.y, data);
+ if (ViewDebug.DEBUG_DRAG) {
+ Log.d(VIEW_LOG_TAG, "performDrag returned " + mAttachInfo.mDragToken);
+ }
+
+ return mAttachInfo.mDragToken != null;
} catch (Exception e) {
Log.e(VIEW_LOG_TAG, "Unable to initiate drag", e);
- mAttachInfo.mDragSurface.destroy();
- mAttachInfo.mDragSurface = null;
+ return false;
+ } finally {
+ if (mAttachInfo.mDragToken == null) {
+ mAttachInfo.mDragSurface.destroy();
+ mAttachInfo.mDragSurface = null;
+ root.setLocalDragState(null);
+ }
+ session.kill();
}
-
- return okay;
}
/**
@@ -26240,53 +26458,6 @@
}
/**
- * Resuable callback for sending
- * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} accessibility event.
- */
- private class SendViewScrolledAccessibilityEvent implements Runnable {
- public volatile boolean mIsPending;
- public int mDeltaX;
- public int mDeltaY;
-
- public void post(int dx, int dy) {
- mDeltaX += dx;
- mDeltaY += dy;
- if (!mIsPending) {
- mIsPending = true;
- postDelayed(this, ViewConfiguration.getSendRecurringAccessibilityEventsInterval());
- }
- }
-
- @Override
- public void run() {
- if (AccessibilityManager.getInstance(mContext).isEnabled()) {
- AccessibilityEvent event = AccessibilityEvent.obtain(
- AccessibilityEvent.TYPE_VIEW_SCROLLED);
- event.setScrollDeltaX(mDeltaX);
- event.setScrollDeltaY(mDeltaY);
- sendAccessibilityEventUnchecked(event);
- }
- reset();
- }
-
- private void reset() {
- mIsPending = false;
- mDeltaX = 0;
- mDeltaY = 0;
- }
- }
-
- /**
- * Remove the pending callback for sending a
- * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} accessibility event.
- */
- private void cancel(@Nullable SendViewScrolledAccessibilityEvent callback) {
- if (callback == null || !callback.mIsPending) return;
- removeCallbacks(callback);
- callback.reset();
- }
-
- /**
* <p>
* This class represents a delegate that can be registered in a {@link View}
* to enhance accessibility support via composition rather via inheritance.
@@ -26902,6 +27073,8 @@
final boolean fromTouch = (mPrivateFlags3 & PFLAG3_FINGER_DOWN) == PFLAG3_FINGER_DOWN;
mTooltipInfo.mTooltipPopup.show(this, x, y, fromTouch, mTooltipInfo.mTooltipText);
mAttachInfo.mTooltipHost = this;
+ // The available accessibility actions have changed
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
return true;
}
@@ -26920,6 +27093,8 @@
if (mAttachInfo != null) {
mAttachInfo.mTooltipHost = null;
}
+ // The available accessibility actions have changed
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
private boolean showLongClickTooltip(int x, int y) {
@@ -26928,8 +27103,8 @@
return showTooltip(x, y, true);
}
- private void showHoverTooltip() {
- showTooltip(mTooltipInfo.mAnchorX, mTooltipInfo.mAnchorY, false);
+ private boolean showHoverTooltip() {
+ return showTooltip(mTooltipInfo.mAnchorX, mTooltipInfo.mAnchorY, false);
}
boolean dispatchTooltipHoverEvent(MotionEvent event) {
diff --git a/android/view/ViewDebug.java b/android/view/ViewDebug.java
index afa9413..b09934e 100644
--- a/android/view/ViewDebug.java
+++ b/android/view/ViewDebug.java
@@ -21,6 +21,7 @@
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
+import android.graphics.Point;
import android.graphics.Rect;
import android.os.Debug;
import android.os.Handler;
@@ -773,16 +774,15 @@
final CountDownLatch latch = new CountDownLatch(1);
final Bitmap[] cache = new Bitmap[1];
- captureView.post(new Runnable() {
- public void run() {
- try {
- cache[0] = captureView.createSnapshot(
- Bitmap.Config.ARGB_8888, 0, skipChildren);
- } catch (OutOfMemoryError e) {
- Log.w("View", "Out of memory for bitmap");
- } finally {
- latch.countDown();
- }
+ captureView.post(() -> {
+ try {
+ CanvasProvider provider = captureView.isHardwareAccelerated()
+ ? new HardwareCanvasProvider() : new SoftwareCanvasProvider();
+ cache[0] = captureView.createSnapshot(provider, skipChildren);
+ } catch (OutOfMemoryError e) {
+ Log.w("View", "Out of memory for bitmap");
+ } finally {
+ latch.countDown();
}
});
@@ -1740,4 +1740,86 @@
}
});
}
+
+ /**
+ * @hide
+ */
+ public static class SoftwareCanvasProvider implements CanvasProvider {
+
+ private Canvas mCanvas;
+ private Bitmap mBitmap;
+ private boolean mEnabledHwBitmapsInSwMode;
+
+ @Override
+ public Canvas getCanvas(View view, int width, int height) {
+ mBitmap = Bitmap.createBitmap(view.getResources().getDisplayMetrics(),
+ width, height, Bitmap.Config.ARGB_8888);
+ if (mBitmap == null) {
+ throw new OutOfMemoryError();
+ }
+ mBitmap.setDensity(view.getResources().getDisplayMetrics().densityDpi);
+
+ if (view.mAttachInfo != null) {
+ mCanvas = view.mAttachInfo.mCanvas;
+ }
+ if (mCanvas == null) {
+ mCanvas = new Canvas();
+ }
+ mEnabledHwBitmapsInSwMode = mCanvas.isHwBitmapsInSwModeEnabled();
+ mCanvas.setBitmap(mBitmap);
+ return mCanvas;
+ }
+
+ @Override
+ public Bitmap createBitmap() {
+ mCanvas.setBitmap(null);
+ mCanvas.setHwBitmapsInSwModeEnabled(mEnabledHwBitmapsInSwMode);
+ return mBitmap;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static class HardwareCanvasProvider implements CanvasProvider {
+
+ private View mView;
+ private Point mSize;
+ private RenderNode mNode;
+ private DisplayListCanvas mCanvas;
+
+ @Override
+ public Canvas getCanvas(View view, int width, int height) {
+ mView = view;
+ mSize = new Point(width, height);
+ mNode = RenderNode.create("ViewDebug", mView);
+ mNode.setLeftTopRightBottom(0, 0, width, height);
+ mNode.setClipToBounds(false);
+ mCanvas = mNode.start(width, height);
+ return mCanvas;
+ }
+
+ @Override
+ public Bitmap createBitmap() {
+ mNode.end(mCanvas);
+ return ThreadedRenderer.createHardwareBitmap(mNode, mSize.x, mSize.y);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public interface CanvasProvider {
+
+ /**
+ * Returns a canvas which can be used to draw {@param view}
+ */
+ Canvas getCanvas(View view, int width, int height);
+
+ /**
+ * Creates a bitmap from previously returned canvas
+ * @return
+ */
+ Bitmap createBitmap();
+ }
}
diff --git a/android/view/ViewGroup.java b/android/view/ViewGroup.java
index 122df93..4631261 100644
--- a/android/view/ViewGroup.java
+++ b/android/view/ViewGroup.java
@@ -57,6 +57,7 @@
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
import android.view.animation.Transformation;
+import android.view.autofill.Helper;
import com.android.internal.R;
@@ -3215,22 +3216,31 @@
}
int descendantFocusability = getDescendantFocusability();
+ boolean result;
switch (descendantFocusability) {
case FOCUS_BLOCK_DESCENDANTS:
- return super.requestFocus(direction, previouslyFocusedRect);
+ result = super.requestFocus(direction, previouslyFocusedRect);
+ break;
case FOCUS_BEFORE_DESCENDANTS: {
final boolean took = super.requestFocus(direction, previouslyFocusedRect);
- return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
+ result = took ? took : onRequestFocusInDescendants(direction,
+ previouslyFocusedRect);
+ break;
}
case FOCUS_AFTER_DESCENDANTS: {
final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
- return took ? took : super.requestFocus(direction, previouslyFocusedRect);
+ result = took ? took : super.requestFocus(direction, previouslyFocusedRect);
+ break;
}
default:
throw new IllegalStateException("descendant focusability must be "
+ "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
+ "but is " + descendantFocusability);
}
+ if (result && !isLayoutValid() && ((mPrivateFlags & PFLAG_WANTS_FOCUS) == 0)) {
+ mPrivateFlags |= PFLAG_WANTS_FOCUS;
+ }
+ return result;
}
/**
@@ -3465,8 +3475,10 @@
}
if (!isLaidOut()) {
- Log.v(VIEW_LOG_TAG, "dispatchProvideStructure(): not laid out, ignoring "
- + childrenCount + " children of " + getAccessibilityViewId());
+ if (Helper.sVerbose) {
+ Log.v(VIEW_LOG_TAG, "dispatchProvideStructure(): not laid out, ignoring "
+ + childrenCount + " children of " + getAccessibilityViewId());
+ }
return;
}
@@ -3637,44 +3649,34 @@
return ViewGroup.class.getName();
}
- @Override
- public void notifySubtreeAccessibilityStateChanged(View child, View source, int changeType) {
- // If this is a live region, we should send a subtree change event
- // from this view. Otherwise, we can let it propagate up.
- if (getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE) {
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
- } else if (mParent != null) {
- try {
- mParent.notifySubtreeAccessibilityStateChanged(this, source, changeType);
- } catch (AbstractMethodError e) {
- Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
- " does not fully implement ViewParent", e);
- }
- }
- }
-
/** @hide */
@Override
- public void notifySubtreeAccessibilityStateChangedIfNeeded() {
+ public void notifyAccessibilitySubtreeChanged() {
if (!AccessibilityManager.getInstance(mContext).isEnabled() || mAttachInfo == null) {
return;
}
// If something important for a11y is happening in this subtree, make sure it's dispatched
// from a view that is important for a11y so it doesn't get lost.
- if ((getImportantForAccessibility() != IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS)
- && !isImportantForAccessibility() && (getChildCount() > 0)) {
+ if (getImportantForAccessibility() != IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ && !isImportantForAccessibility()
+ && getChildCount() > 0) {
ViewParent a11yParent = getParentForAccessibility();
if (a11yParent instanceof View) {
- ((View) a11yParent).notifySubtreeAccessibilityStateChangedIfNeeded();
+ ((View) a11yParent).notifyAccessibilitySubtreeChanged();
return;
}
}
- super.notifySubtreeAccessibilityStateChangedIfNeeded();
+ super.notifyAccessibilitySubtreeChanged();
}
@Override
- void resetSubtreeAccessibilityStateChanged() {
+ public void notifySubtreeAccessibilityStateChanged(View child, View source, int changeType) {
+ notifyAccessibilityStateChanged(source, changeType);
+ }
+
+ /** @hide */
+ @Override
+ public void resetSubtreeAccessibilityStateChanged() {
super.resetSubtreeAccessibilityStateChanged();
View[] children = mChildren;
final int childCount = mChildrenCount;
@@ -3854,7 +3856,7 @@
* @hide
*/
@Override
- public Bitmap createSnapshot(Bitmap.Config quality, int backgroundColor, boolean skipChildren) {
+ public Bitmap createSnapshot(ViewDebug.CanvasProvider canvasProvider, boolean skipChildren) {
int count = mChildrenCount;
int[] visibilities = null;
@@ -3870,17 +3872,17 @@
}
}
- Bitmap b = super.createSnapshot(quality, backgroundColor, skipChildren);
-
- if (skipChildren) {
- for (int i = 0; i < count; i++) {
- View child = getChildAt(i);
- child.mViewFlags = (child.mViewFlags & ~View.VISIBILITY_MASK)
- | (visibilities[i] & View.VISIBILITY_MASK);
+ try {
+ return super.createSnapshot(canvasProvider, skipChildren);
+ } finally {
+ if (skipChildren) {
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ child.mViewFlags = (child.mViewFlags & ~View.VISIBILITY_MASK)
+ | (visibilities[i] & View.VISIBILITY_MASK);
+ }
}
}
-
- return b;
}
/** Return true if this ViewGroup is laying out using optical bounds. */
@@ -5086,7 +5088,7 @@
}
if (child.getVisibility() != View.GONE) {
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
if (mTransientIndices != null) {
@@ -5356,7 +5358,7 @@
dispatchViewRemoved(view);
if (view.getVisibility() != View.GONE) {
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
@@ -6075,7 +6077,7 @@
if (invalidate) {
invalidateViewProperty(false, false);
}
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
@Override
diff --git a/android/view/ViewRootImpl.java b/android/view/ViewRootImpl.java
index 6c5091c..30f584c 100644
--- a/android/view/ViewRootImpl.java
+++ b/android/view/ViewRootImpl.java
@@ -20,7 +20,7 @@
import static android.view.View.PFLAG_DRAW_ANIMATION;
import static android.view.WindowCallbacks.RESIZE_MODE_DOCKED_DIVIDER;
import static android.view.WindowCallbacks.RESIZE_MODE_FREEFORM;
-import static android.view.WindowManager.LayoutParams.FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL;
@@ -89,9 +89,11 @@
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.accessibility.AccessibilityViewHierarchyState;
import android.view.accessibility.AccessibilityWindowInfo;
import android.view.accessibility.IAccessibilityInteractionConnection;
import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
+import android.view.accessibility.ThrottlingAccessibilityEventSender;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.inputmethod.InputMethodManager;
@@ -113,7 +115,6 @@
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
-import java.util.HashSet;
import java.util.concurrent.CountDownLatch;
/**
@@ -460,10 +461,6 @@
new AccessibilityInteractionConnectionManager();
final HighContrastTextManager mHighContrastTextManager;
- SendWindowContentChangedAccessibilityEvent mSendWindowContentChangedAccessibilityEvent;
-
- HashSet<View> mTempHashSet;
-
private final int mDensity;
private final int mNoncompatDensity;
@@ -478,6 +475,8 @@
private boolean mNeedsRendererSetup;
+ protected AccessibilityViewHierarchyState mAccessibilityState;
+
/**
* Consistency verifier for debugging purposes.
*/
@@ -531,7 +530,7 @@
mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
if (!sCompatibilityDone) {
- sAlwaysAssignFocus = true;
+ sAlwaysAssignFocus = mTargetSdkVersion < Build.VERSION_CODES.P;
sCompatibilityDone = true;
}
@@ -1597,9 +1596,9 @@
void dispatchApplyInsets(View host) {
WindowInsets insets = getWindowInsets(true /* forceConstruct */);
- final boolean layoutInCutout =
- (mWindowAttributes.flags2 & FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA) != 0;
- if (!layoutInCutout) {
+ final boolean dispatchCutout = (mWindowAttributes.layoutInDisplayCutoutMode
+ == LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS);
+ if (!dispatchCutout) {
// Window is either not laid out in cutout or the status bar inset takes care of
// clearing the cutout, so we don't need to dispatch the cutout to the hierarchy.
insets = insets.consumeDisplayCutout();
@@ -2338,7 +2337,7 @@
}
if (mFirst) {
- if (sAlwaysAssignFocus) {
+ if (sAlwaysAssignFocus || !isInTouchMode()) {
// handle first focus request
if (DEBUG_INPUT_RESIZE) {
Log.v(mTag, "First: mView.hasFocus()=" + mView.hasFocus());
@@ -3609,7 +3608,7 @@
checkThread();
if (mView != null) {
if (!mView.hasFocus()) {
- if (sAlwaysAssignFocus) {
+ if (sAlwaysAssignFocus || !isInTouchMode()) {
v.requestFocus();
}
} else {
@@ -4212,10 +4211,7 @@
// find the best view to give focus to in this brave new non-touch-mode
// world
- final View focused = focusSearch(null, View.FOCUS_DOWN);
- if (focused != null) {
- return focused.requestFocus(View.FOCUS_DOWN);
- }
+ return mView.restoreDefaultFocus();
}
return false;
}
@@ -7262,11 +7258,9 @@
* {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()}.
*/
private void postSendWindowContentChangedCallback(View source, int changeType) {
- if (mSendWindowContentChangedAccessibilityEvent == null) {
- mSendWindowContentChangedAccessibilityEvent =
- new SendWindowContentChangedAccessibilityEvent();
- }
- mSendWindowContentChangedAccessibilityEvent.runOrPost(source, changeType);
+ getAccessibilityState()
+ .getSendWindowContentChangedAccessibilityEvent()
+ .runOrPost(source, changeType);
}
/**
@@ -7274,11 +7268,20 @@
* {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} event.
*/
private void removeSendWindowContentChangedCallback() {
- if (mSendWindowContentChangedAccessibilityEvent != null) {
- mHandler.removeCallbacks(mSendWindowContentChangedAccessibilityEvent);
+ if (mAccessibilityState != null
+ && mAccessibilityState.isWindowContentChangedEventSenderInitialized()) {
+ ThrottlingAccessibilityEventSender.cancelIfPending(
+ mAccessibilityState.getSendWindowContentChangedAccessibilityEvent());
}
}
+ AccessibilityViewHierarchyState getAccessibilityState() {
+ if (mAccessibilityState == null) {
+ mAccessibilityState = new AccessibilityViewHierarchyState();
+ }
+ return mAccessibilityState;
+ }
+
@Override
public boolean showContextMenuForChild(View originalView) {
return false;
@@ -7314,12 +7317,8 @@
return false;
}
- // Immediately flush pending content changed event (if any) to preserve event order
- if (event.getEventType() != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
- && mSendWindowContentChangedAccessibilityEvent != null
- && mSendWindowContentChangedAccessibilityEvent.mSource != null) {
- mSendWindowContentChangedAccessibilityEvent.removeCallbacksAndRun();
- }
+ // Send any pending event to prevent reordering
+ flushPendingAccessibilityEvents();
// Intercept accessibility focus events fired by virtual nodes to keep
// track of accessibility focus position in such nodes.
@@ -7363,6 +7362,19 @@
return true;
}
+ /** @hide */
+ public void flushPendingAccessibilityEvents() {
+ if (mAccessibilityState != null) {
+ if (mAccessibilityState.isScrollEventSenderInitialized()) {
+ mAccessibilityState.getSendViewScrolledAccessibilityEvent().sendNowIfPending();
+ }
+ if (mAccessibilityState.isWindowContentChangedEventSenderInitialized()) {
+ mAccessibilityState.getSendWindowContentChangedAccessibilityEvent()
+ .sendNowIfPending();
+ }
+ }
+ }
+
/**
* Updates the focused virtual view, when necessary, in response to a
* content changed event.
@@ -7497,39 +7509,6 @@
return View.TEXT_ALIGNMENT_RESOLVED_DEFAULT;
}
- private View getCommonPredecessor(View first, View second) {
- if (mTempHashSet == null) {
- mTempHashSet = new HashSet<View>();
- }
- HashSet<View> seen = mTempHashSet;
- seen.clear();
- View firstCurrent = first;
- while (firstCurrent != null) {
- seen.add(firstCurrent);
- ViewParent firstCurrentParent = firstCurrent.mParent;
- if (firstCurrentParent instanceof View) {
- firstCurrent = (View) firstCurrentParent;
- } else {
- firstCurrent = null;
- }
- }
- View secondCurrent = second;
- while (secondCurrent != null) {
- if (seen.contains(secondCurrent)) {
- seen.clear();
- return secondCurrent;
- }
- ViewParent secondCurrentParent = secondCurrent.mParent;
- if (secondCurrentParent instanceof View) {
- secondCurrent = (View) secondCurrentParent;
- } else {
- secondCurrent = null;
- }
- }
- seen.clear();
- return null;
- }
-
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
@@ -8140,80 +8119,6 @@
}
}
- private class SendWindowContentChangedAccessibilityEvent implements Runnable {
- private int mChangeTypes = 0;
-
- public View mSource;
- public long mLastEventTimeMillis;
-
- @Override
- public void run() {
- // Protect against re-entrant code and attempt to do the right thing in the case that
- // we're multithreaded.
- View source = mSource;
- mSource = null;
- if (source == null) {
- Log.e(TAG, "Accessibility content change has no source");
- return;
- }
- // The accessibility may be turned off while we were waiting so check again.
- if (AccessibilityManager.getInstance(mContext).isEnabled()) {
- mLastEventTimeMillis = SystemClock.uptimeMillis();
- AccessibilityEvent event = AccessibilityEvent.obtain();
- event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
- event.setContentChangeTypes(mChangeTypes);
- source.sendAccessibilityEventUnchecked(event);
- } else {
- mLastEventTimeMillis = 0;
- }
- // In any case reset to initial state.
- source.resetSubtreeAccessibilityStateChanged();
- mChangeTypes = 0;
- }
-
- public void runOrPost(View source, int changeType) {
- if (mHandler.getLooper() != Looper.myLooper()) {
- CalledFromWrongThreadException e = new CalledFromWrongThreadException("Only the "
- + "original thread that created a view hierarchy can touch its views.");
- // TODO: Throw the exception
- Log.e(TAG, "Accessibility content change on non-UI thread. Future Android "
- + "versions will throw an exception.", e);
- // Attempt to recover. This code does not eliminate the thread safety issue, but
- // it should force any issues to happen near the above log.
- mHandler.removeCallbacks(this);
- if (mSource != null) {
- // Dispatch whatever was pending. It's still possible that the runnable started
- // just before we removed the callbacks, and bad things will happen, but at
- // least they should happen very close to the logged error.
- run();
- }
- }
- if (mSource != null) {
- // If there is no common predecessor, then mSource points to
- // a removed view, hence in this case always prefer the source.
- View predecessor = getCommonPredecessor(mSource, source);
- mSource = (predecessor != null) ? predecessor : source;
- mChangeTypes |= changeType;
- return;
- }
- mSource = source;
- mChangeTypes = changeType;
- final long timeSinceLastMillis = SystemClock.uptimeMillis() - mLastEventTimeMillis;
- final long minEventIntevalMillis =
- ViewConfiguration.getSendRecurringAccessibilityEventsInterval();
- if (timeSinceLastMillis >= minEventIntevalMillis) {
- removeCallbacksAndRun();
- } else {
- mHandler.postDelayed(this, minEventIntevalMillis - timeSinceLastMillis);
- }
- }
-
- public void removeCallbacksAndRun() {
- mHandler.removeCallbacks(this);
- run();
- }
- }
-
private static class KeyFallbackManager {
// This is used to ensure that key-fallback events are only dispatched once. We attempt
diff --git a/android/view/ViewStructure.java b/android/view/ViewStructure.java
index d665dde..1d94abe 100644
--- a/android/view/ViewStructure.java
+++ b/android/view/ViewStructure.java
@@ -26,6 +26,8 @@
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
+import com.android.internal.util.Preconditions;
+
import java.util.List;
/**
@@ -204,6 +206,16 @@
public abstract void setTextLines(int[] charOffsets, int[] baselines);
/**
+ * Sets the identifier used to set the text associated with this view.
+ *
+ * <p>Should only be set when the node is used for autofill purposes - it will be ignored
+ * when used for Assist.
+ */
+ public void setTextIdEntry(@NonNull String entryName) {
+ Preconditions.checkNotNull(entryName);
+ }
+
+ /**
* Set optional hint text associated with this view; this is for example the text that is
* shown by an EditText when it is empty to indicate to the user the kind of text to input.
*/
diff --git a/android/view/Window.java b/android/view/Window.java
index 176927f..5bd0782 100644
--- a/android/view/Window.java
+++ b/android/view/Window.java
@@ -1339,9 +1339,9 @@
/**
* Finds a view that was identified by the {@code android:id} XML attribute
- * that was processed in {@link android.app.Activity#onCreate}. This will
- * implicitly call {@link #getDecorView} with all of the associated
- * side-effects.
+ * that was processed in {@link android.app.Activity#onCreate}.
+ * <p>
+ * This will implicitly call {@link #getDecorView} with all of the associated side-effects.
* <p>
* <strong>Note:</strong> In most cases -- depending on compiler support --
* the resulting view is automatically cast to the target class type. If
@@ -1351,11 +1351,35 @@
* @param id the ID to search for
* @return a view with given ID if found, or {@code null} otherwise
* @see View#findViewById(int)
+ * @see Window#requireViewById(int)
*/
@Nullable
public <T extends View> T findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}
+ /**
+ * Finds a view that was identified by the {@code android:id} XML attribute
+ * that was processed in {@link android.app.Activity#onCreate}, or throws an
+ * IllegalArgumentException if the ID is invalid, or there is no matching view in the hierarchy.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see View#requireViewById(int)
+ * @see Window#findViewById(int)
+ */
+ @NonNull
+ public final <T extends View> T requireViewById(@IdRes int id) {
+ T view = findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException("ID does not reference a View inside this Window");
+ }
+ return view;
+ }
/**
* Convenience for
@@ -2244,9 +2268,36 @@
* <p>
* The transitionName for the view background will be "android:navigation:background".
* </p>
+ * @attr ref android.R.styleable#Window_navigationBarColor
*/
public abstract void setNavigationBarColor(@ColorInt int color);
+ /**
+ * Shows a thin line of the specified color between the navigation bar and the app
+ * content.
+ * <p>
+ * For this to take effect,
+ * the window must be drawing the system bar backgrounds with
+ * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and
+ * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_NAVIGATION} must not be set.
+ *
+ * @param dividerColor The color of the thin line.
+ * @attr ref android.R.styleable#Window_navigationBarDividerColor
+ */
+ public void setNavigationBarDividerColor(@ColorInt int dividerColor) {
+ }
+
+ /**
+ * Retrieves the color of the navigation bar divider.
+ *
+ * @return The color of the navigation bar divider color.
+ * @see #setNavigationBarColor(int)
+ * @attr ref android.R.styleable#Window_navigationBarDividerColor
+ */
+ public @ColorInt int getNavigationBarDividerColor() {
+ return 0;
+ }
+
/** @hide */
public void setTheme(int resId) {
}
diff --git a/android/view/WindowManager.java b/android/view/WindowManager.java
index cbe012a..1c5e871 100644
--- a/android/view/WindowManager.java
+++ b/android/view/WindowManager.java
@@ -17,10 +17,34 @@
package android.view;
import static android.content.pm.ActivityInfo.COLOR_MODE_DEFAULT;
+import static android.view.WindowLayoutParamsProto.ALPHA;
+import static android.view.WindowLayoutParamsProto.BUTTON_BRIGHTNESS;
+import static android.view.WindowLayoutParamsProto.COLOR_MODE;
+import static android.view.WindowLayoutParamsProto.FLAGS;
+import static android.view.WindowLayoutParamsProto.FORMAT;
+import static android.view.WindowLayoutParamsProto.GRAVITY;
+import static android.view.WindowLayoutParamsProto.HAS_SYSTEM_UI_LISTENERS;
+import static android.view.WindowLayoutParamsProto.HEIGHT;
+import static android.view.WindowLayoutParamsProto.HORIZONTAL_MARGIN;
+import static android.view.WindowLayoutParamsProto.INPUT_FEATURE_FLAGS;
+import static android.view.WindowLayoutParamsProto.NEEDS_MENU_KEY;
+import static android.view.WindowLayoutParamsProto.PREFERRED_REFRESH_RATE;
+import static android.view.WindowLayoutParamsProto.PRIVATE_FLAGS;
+import static android.view.WindowLayoutParamsProto.ROTATION_ANIMATION;
+import static android.view.WindowLayoutParamsProto.SCREEN_BRIGHTNESS;
+import static android.view.WindowLayoutParamsProto.SOFT_INPUT_MODE;
+import static android.view.WindowLayoutParamsProto.SUBTREE_SYSTEM_UI_VISIBILITY_FLAGS;
+import static android.view.WindowLayoutParamsProto.SYSTEM_UI_VISIBILITY_FLAGS;
+import static android.view.WindowLayoutParamsProto.TYPE;
+import static android.view.WindowLayoutParamsProto.USER_ACTIVITY_TIMEOUT;
+import static android.view.WindowLayoutParamsProto.VERTICAL_MARGIN;
+import static android.view.WindowLayoutParamsProto.WIDTH;
+import static android.view.WindowLayoutParamsProto.WINDOW_ANIMATIONS;
+import static android.view.WindowLayoutParamsProto.X;
+import static android.view.WindowLayoutParamsProto.Y;
import android.Manifest.permission;
import android.annotation.IntDef;
-import android.annotation.LongDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
@@ -74,11 +98,198 @@
int DOCKED_BOTTOM = 4;
/** @hide */
- final static String INPUT_CONSUMER_PIP = "pip_input_consumer";
+ String INPUT_CONSUMER_PIP = "pip_input_consumer";
/** @hide */
- final static String INPUT_CONSUMER_NAVIGATION = "nav_input_consumer";
+ String INPUT_CONSUMER_NAVIGATION = "nav_input_consumer";
/** @hide */
- final static String INPUT_CONSUMER_WALLPAPER = "wallpaper_input_consumer";
+ String INPUT_CONSUMER_WALLPAPER = "wallpaper_input_consumer";
+ /** @hide */
+ String INPUT_CONSUMER_RECENTS_ANIMATION = "recents_animation_input_consumer";
+
+ /**
+ * Not set up for a transition.
+ * @hide
+ */
+ int TRANSIT_UNSET = -1;
+
+ /**
+ * No animation for transition.
+ * @hide
+ */
+ int TRANSIT_NONE = 0;
+
+ /**
+ * A window in a new activity is being opened on top of an existing one in the same task.
+ * @hide
+ */
+ int TRANSIT_ACTIVITY_OPEN = 6;
+
+ /**
+ * The window in the top-most activity is being closed to reveal the previous activity in the
+ * same task.
+ * @hide
+ */
+ int TRANSIT_ACTIVITY_CLOSE = 7;
+
+ /**
+ * A window in a new task is being opened on top of an existing one in another activity's task.
+ * @hide
+ */
+ int TRANSIT_TASK_OPEN = 8;
+
+ /**
+ * A window in the top-most activity is being closed to reveal the previous activity in a
+ * different task.
+ * @hide
+ */
+ int TRANSIT_TASK_CLOSE = 9;
+
+ /**
+ * A window in an existing task is being displayed on top of an existing one in another
+ * activity's task.
+ * @hide
+ */
+ int TRANSIT_TASK_TO_FRONT = 10;
+
+ /**
+ * A window in an existing task is being put below all other tasks.
+ * @hide
+ */
+ int TRANSIT_TASK_TO_BACK = 11;
+
+ /**
+ * A window in a new activity that doesn't have a wallpaper is being opened on top of one that
+ * does, effectively closing the wallpaper.
+ * @hide
+ */
+ int TRANSIT_WALLPAPER_CLOSE = 12;
+
+ /**
+ * A window in a new activity that does have a wallpaper is being opened on one that didn't,
+ * effectively opening the wallpaper.
+ * @hide
+ */
+ int TRANSIT_WALLPAPER_OPEN = 13;
+
+ /**
+ * A window in a new activity is being opened on top of an existing one, and both are on top
+ * of the wallpaper.
+ * @hide
+ */
+ int TRANSIT_WALLPAPER_INTRA_OPEN = 14;
+
+ /**
+ * The window in the top-most activity is being closed to reveal the previous activity, and
+ * both are on top of the wallpaper.
+ * @hide
+ */
+ int TRANSIT_WALLPAPER_INTRA_CLOSE = 15;
+
+ /**
+ * A window in a new task is being opened behind an existing one in another activity's task.
+ * The new window will show briefly and then be gone.
+ * @hide
+ */
+ int TRANSIT_TASK_OPEN_BEHIND = 16;
+
+ /**
+ * A window in a task is being animated in-place.
+ * @hide
+ */
+ int TRANSIT_TASK_IN_PLACE = 17;
+
+ /**
+ * An activity is being relaunched (e.g. due to configuration change).
+ * @hide
+ */
+ int TRANSIT_ACTIVITY_RELAUNCH = 18;
+
+ /**
+ * A task is being docked from recents.
+ * @hide
+ */
+ int TRANSIT_DOCK_TASK_FROM_RECENTS = 19;
+
+ /**
+ * Keyguard is going away.
+ * @hide
+ */
+ int TRANSIT_KEYGUARD_GOING_AWAY = 20;
+
+ /**
+ * Keyguard is going away with showing an activity behind that requests wallpaper.
+ * @hide
+ */
+ int TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER = 21;
+
+ /**
+ * Keyguard is being occluded.
+ * @hide
+ */
+ int TRANSIT_KEYGUARD_OCCLUDE = 22;
+
+ /**
+ * Keyguard is being unoccluded.
+ * @hide
+ */
+ int TRANSIT_KEYGUARD_UNOCCLUDE = 23;
+
+ /**
+ * @hide
+ */
+ @IntDef(prefix = { "TRANSIT_" }, value = {
+ TRANSIT_UNSET,
+ TRANSIT_NONE,
+ TRANSIT_ACTIVITY_OPEN,
+ TRANSIT_ACTIVITY_CLOSE,
+ TRANSIT_TASK_OPEN,
+ TRANSIT_TASK_CLOSE,
+ TRANSIT_TASK_TO_FRONT,
+ TRANSIT_TASK_TO_BACK,
+ TRANSIT_WALLPAPER_CLOSE,
+ TRANSIT_WALLPAPER_OPEN,
+ TRANSIT_WALLPAPER_INTRA_OPEN,
+ TRANSIT_WALLPAPER_INTRA_CLOSE,
+ TRANSIT_TASK_OPEN_BEHIND,
+ TRANSIT_TASK_IN_PLACE,
+ TRANSIT_ACTIVITY_RELAUNCH,
+ TRANSIT_DOCK_TASK_FROM_RECENTS,
+ TRANSIT_KEYGUARD_GOING_AWAY,
+ TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER,
+ TRANSIT_KEYGUARD_OCCLUDE,
+ TRANSIT_KEYGUARD_UNOCCLUDE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface TransitionType {}
+
+ /**
+ * Transition flag: Keyguard is going away, but keeping the notification shade open
+ * @hide
+ */
+ int TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE = 0x1;
+
+ /**
+ * Transition flag: Keyguard is going away, but doesn't want an animation for it
+ * @hide
+ */
+ int TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION = 0x2;
+
+ /**
+ * Transition flag: Keyguard is going away while it was showing the system wallpaper.
+ * @hide
+ */
+ int TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER = 0x4;
+
+ /**
+ * @hide
+ */
+ @IntDef(flag = true, prefix = { "TRANSIT_FLAG_" }, value = {
+ TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE,
+ TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION,
+ TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface TransitionFlags {}
/**
* Exception that is thrown when trying to add view whose
@@ -863,7 +1074,12 @@
* decorations around the border (such as the status bar). The
* window must correctly position its contents to take the screen
* decoration into account. This flag is normally set for you
- * by Window as described in {@link Window#setFlags}. */
+ * by Window as described in {@link Window#setFlags}.
+ *
+ * <p>Note: on displays that have a {@link DisplayCutout}, the window may be placed
+ * such that it avoids the {@link DisplayCutout} area if necessary according to the
+ * {@link #layoutInDisplayCutoutMode}.
+ */
public static final int FLAG_LAYOUT_IN_SCREEN = 0x00000100;
/** Window flag: allow window to extend outside of the screen. */
@@ -1269,33 +1485,6 @@
}, formatToHexString = true)
public int flags;
- /** @hide */
- @Retention(RetentionPolicy.SOURCE)
- @LongDef(
- flag = true,
- value = {
- LayoutParams.FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA,
- })
- @interface Flags2 {}
-
- /**
- * Window flag: allow placing the window within the area that overlaps with the
- * display cutout.
- *
- * <p>
- * The window must correctly position its contents to take the display cutout into account.
- *
- * @see DisplayCutout
- */
- public static final long FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA = 0x00000001;
-
- /**
- * Various behavioral options/flags. Default is none.
- *
- * @see #FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA
- */
- @Flags2 public long flags2;
-
/**
* If the window has requested hardware acceleration, but this is not
* allowed in the process it is in, then still render it as if it is
@@ -2024,6 +2213,77 @@
*/
public boolean hasSystemUiListeners;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT,
+ LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS,
+ LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER})
+ @interface LayoutInDisplayCutoutMode {}
+
+ /**
+ * Controls how the window is laid out if there is a {@link DisplayCutout}.
+ *
+ * <p>
+ * Defaults to {@link #LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT}.
+ *
+ * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
+ * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+ * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
+ * @see DisplayCutout
+ */
+ @LayoutInDisplayCutoutMode
+ public int layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
+
+ /**
+ * The window is allowed to extend into the {@link DisplayCutout} area, only if the
+ * {@link DisplayCutout} is fully contained within the status bar. Otherwise, the window is
+ * laid out such that it does not overlap with the {@link DisplayCutout} area.
+ *
+ * <p>
+ * In practice, this means that if the window did not set FLAG_FULLSCREEN or
+ * SYSTEM_UI_FLAG_FULLSCREEN, it can extend into the cutout area in portrait.
+ * Otherwise (i.e. fullscreen or landscape) it is laid out such that it does overlap the
+ * cutout area.
+ *
+ * <p>
+ * The usual precautions for not overlapping with the status bar are sufficient for ensuring
+ * that no important content overlaps with the DisplayCutout.
+ *
+ * @see DisplayCutout
+ * @see WindowInsets
+ */
+ public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT = 0;
+
+ /**
+ * The window is always allowed to extend into the {@link DisplayCutout} area,
+ * even if fullscreen or in landscape.
+ *
+ * <p>
+ * The window must make sure that no important content overlaps with the
+ * {@link DisplayCutout}.
+ *
+ * @see DisplayCutout
+ * @see WindowInsets#getDisplayCutout()
+ */
+ public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS = 1;
+
+ /**
+ * The window is never allowed to overlap with the DisplayCutout area.
+ *
+ * <p>
+ * This should be used with windows that transiently set SYSTEM_UI_FLAG_FULLSCREEN to
+ * avoid a relayout of the window when the flag is set or cleared.
+ *
+ * @see DisplayCutout
+ * @see View#SYSTEM_UI_FLAG_FULLSCREEN SYSTEM_UI_FLAG_FULLSCREEN
+ * @see View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ */
+ public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER = 2;
+
+
/**
* When this window has focus, disable touch pad pointer gesture processing.
* The window will receive raw position updates from the touch pad instead
@@ -2247,9 +2507,9 @@
out.writeInt(y);
out.writeInt(type);
out.writeInt(flags);
- out.writeLong(flags2);
out.writeInt(privateFlags);
out.writeInt(softInputMode);
+ out.writeInt(layoutInDisplayCutoutMode);
out.writeInt(gravity);
out.writeFloat(horizontalMargin);
out.writeFloat(verticalMargin);
@@ -2303,9 +2563,9 @@
y = in.readInt();
type = in.readInt();
flags = in.readInt();
- flags2 = in.readLong();
privateFlags = in.readInt();
softInputMode = in.readInt();
+ layoutInDisplayCutoutMode = in.readInt();
gravity = in.readInt();
horizontalMargin = in.readFloat();
verticalMargin = in.readFloat();
@@ -2436,10 +2696,6 @@
flags = o.flags;
changes |= FLAGS_CHANGED;
}
- if (flags2 != o.flags2) {
- flags2 = o.flags2;
- changes |= FLAGS_CHANGED;
- }
if (privateFlags != o.privateFlags) {
privateFlags = o.privateFlags;
changes |= PRIVATE_FLAGS_CHANGED;
@@ -2448,6 +2704,10 @@
softInputMode = o.softInputMode;
changes |= SOFT_INPUT_MODE_CHANGED;
}
+ if (layoutInDisplayCutoutMode != o.layoutInDisplayCutoutMode) {
+ layoutInDisplayCutoutMode = o.layoutInDisplayCutoutMode;
+ changes |= LAYOUT_CHANGED;
+ }
if (gravity != o.gravity) {
gravity = o.gravity;
changes |= LAYOUT_CHANGED;
@@ -2625,6 +2885,10 @@
sb.append(softInputModeToString(softInputMode));
sb.append('}');
}
+ if (layoutInDisplayCutoutMode != 0) {
+ sb.append(" layoutInDisplayCutoutMode=");
+ sb.append(layoutInDisplayCutoutModeToString(layoutInDisplayCutoutMode));
+ }
sb.append(" ty=");
sb.append(ViewDebug.intToString(LayoutParams.class, "type", type));
if (format != PixelFormat.OPAQUE) {
@@ -2693,11 +2957,6 @@
sb.append(System.lineSeparator());
sb.append(prefix).append(" fl=").append(
ViewDebug.flagsToString(LayoutParams.class, "flags", flags));
- if (flags2 != 0) {
- sb.append(System.lineSeparator());
- // TODO(roosa): add a long overload for ViewDebug.flagsToString.
- sb.append(prefix).append(" fl2=0x").append(Long.toHexString(flags2));
- }
if (privateFlags != 0) {
sb.append(System.lineSeparator());
sb.append(prefix).append(" pfl=").append(ViewDebug.flagsToString(
@@ -2722,7 +2981,32 @@
*/
public void writeToProto(ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
- proto.write(WindowLayoutParamsProto.TYPE, type);
+ proto.write(TYPE, type);
+ proto.write(X, x);
+ proto.write(Y, y);
+ proto.write(WIDTH, width);
+ proto.write(HEIGHT, height);
+ proto.write(HORIZONTAL_MARGIN, horizontalMargin);
+ proto.write(VERTICAL_MARGIN, verticalMargin);
+ proto.write(GRAVITY, gravity);
+ proto.write(SOFT_INPUT_MODE, softInputMode);
+ proto.write(FORMAT, format);
+ proto.write(WINDOW_ANIMATIONS, windowAnimations);
+ proto.write(ALPHA, alpha);
+ proto.write(SCREEN_BRIGHTNESS, screenBrightness);
+ proto.write(BUTTON_BRIGHTNESS, buttonBrightness);
+ proto.write(ROTATION_ANIMATION, rotationAnimation);
+ proto.write(PREFERRED_REFRESH_RATE, preferredRefreshRate);
+ proto.write(WindowLayoutParamsProto.PREFERRED_DISPLAY_MODE_ID, preferredDisplayModeId);
+ proto.write(HAS_SYSTEM_UI_LISTENERS, hasSystemUiListeners);
+ proto.write(INPUT_FEATURE_FLAGS, inputFeatures);
+ proto.write(USER_ACTIVITY_TIMEOUT, userActivityTimeout);
+ proto.write(NEEDS_MENU_KEY, needsMenuKey);
+ proto.write(COLOR_MODE, mColorMode);
+ proto.write(FLAGS, flags);
+ proto.write(PRIVATE_FLAGS, privateFlags);
+ proto.write(SYSTEM_UI_VISIBILITY_FLAGS, systemUiVisibility);
+ proto.write(SUBTREE_SYSTEM_UI_VISIBILITY_FLAGS, subtreeSystemUiVisibility);
proto.end(token);
}
@@ -2797,6 +3081,20 @@
&& height == WindowManager.LayoutParams.MATCH_PARENT;
}
+ private static String layoutInDisplayCutoutModeToString(
+ @LayoutInDisplayCutoutMode int mode) {
+ switch (mode) {
+ case LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT:
+ return "default";
+ case LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS:
+ return "always";
+ case LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER:
+ return "never";
+ default:
+ return "unknown(" + mode + ")";
+ }
+ }
+
private static String softInputModeToString(@SoftInputModeFlags int softInputMode) {
final StringBuilder result = new StringBuilder();
final int state = softInputMode & SOFT_INPUT_MASK_STATE;
diff --git a/android/view/WindowManagerPolicyConstants.java b/android/view/WindowManagerPolicyConstants.java
index 21943bd..a6f36bb 100644
--- a/android/view/WindowManagerPolicyConstants.java
+++ b/android/view/WindowManagerPolicyConstants.java
@@ -18,8 +18,6 @@
import static android.view.Display.DEFAULT_DISPLAY;
-import android.annotation.SystemApi;
-
/**
* Constants for interfacing with WindowManagerService and WindowManagerPolicyInternal.
* @hide
@@ -47,6 +45,11 @@
int PRESENCE_INTERNAL = 1 << 0;
int PRESENCE_EXTERNAL = 1 << 1;
+ // Navigation bar position values
+ int NAV_BAR_LEFT = 1 << 0;
+ int NAV_BAR_RIGHT = 1 << 1;
+ int NAV_BAR_BOTTOM = 1 << 2;
+
/**
* Sticky broadcast of the current HDMI plugged state.
*/
@@ -62,7 +65,6 @@
* Set to {@code true} when intent was invoked from pressing the home key.
* @hide
*/
- @SystemApi
String EXTRA_FROM_HOME_KEY = "android.intent.extra.FROM_HOME_KEY";
// TODO: move this to a more appropriate place.
diff --git a/android/view/accessibility/AccessibilityEvent.java b/android/view/accessibility/AccessibilityEvent.java
index 1d19a9f..e0f74a7 100644
--- a/android/view/accessibility/AccessibilityEvent.java
+++ b/android/view/accessibility/AccessibilityEvent.java
@@ -38,19 +38,14 @@
* <p>
* An accessibility event is fired by an individual view which populates the event with
* data for its state and requests from its parent to send the event to interested
- * parties. The parent can optionally add an {@link AccessibilityRecord} for itself before
- * dispatching a similar request to its parent. A parent can also choose not to respect the
- * request for sending an event. The accessibility event is sent by the topmost view in the
- * view tree. Therefore, an {@link android.accessibilityservice.AccessibilityService} can
- * explore all records in an accessibility event to obtain more information about the
- * context in which the event was fired.
+ * parties. The parent can optionally modify or even block the event based on its broader
+ * understanding of the user interface's context.
* </p>
* <p>
- * The main purpose of an accessibility event is to expose enough information for an
- * {@link android.accessibilityservice.AccessibilityService} to provide meaningful feedback
- * to the user. Sometimes however, an accessibility service may need more contextual
- * information then the one in the event pay-load. In such cases the service can obtain
- * the event source which is an {@link AccessibilityNodeInfo} (snapshot of a View state)
+ * The main purpose of an accessibility event is to communicate changes in the UI to an
+ * {@link android.accessibilityservice.AccessibilityService}. The service may then inspect,
+ * if needed the user interface by examining the View hierarchy, as represented by a tree of
+ * {@link AccessibilityNodeInfo}s (snapshot of a View state)
* which can be used for exploring the window content. Note that the privilege for accessing
* an event's source, thus the window content, has to be explicitly requested. For more
* details refer to {@link android.accessibilityservice.AccessibilityService}. If an
@@ -85,21 +80,6 @@
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source's sub-tree.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #isPassword()} - Whether the source is password.</li>
- * <li>{@link #isChecked()} - Whether the source is checked.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
- * <li>{@link #getScrollX()} - The offset of the source left edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getScrollY()} - The offset of the source top edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getFromIndex()} - The zero based index of the first visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getToIndex()} - The zero based index of the last visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getItemCount()} - The total items of the source
- * (for descendants of AdapterView).</li>
* </ul>
* </p>
* <p>
@@ -113,21 +93,6 @@
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source's sub-tree.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #isPassword()} - Whether the source is password.</li>
- * <li>{@link #isChecked()} - Whether the source is checked.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
- * <li>{@link #getScrollX()} - The offset of the source left edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getScrollY()} - The offset of the source top edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getFromIndex()} - The zero based index of the first visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getToIndex()} - The zero based index of the last visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getItemCount()} - The total items of the source
- * (for descendants of AdapterView).</li>
* </ul>
* </p>
* <p>
@@ -141,23 +106,6 @@
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source's sub-tree.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #isPassword()} - Whether the source is password.</li>
- * <li>{@link #isChecked()} - Whether the source is checked.</li>
- * <li>{@link #getItemCount()} - The number of selectable items of the source.</li>
- * <li>{@link #getCurrentItemIndex()} - The currently selected item index.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
- * <li>{@link #getScrollX()} - The offset of the source left edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getScrollY()} - The offset of the source top edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getFromIndex()} - The zero based index of the first visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getToIndex()} - The zero based index of the last visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getItemCount()} - The total items of the source
- * (for descendants of AdapterView).</li>
* </ul>
* </p>
* <p>
@@ -171,23 +119,6 @@
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source's sub-tree.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #isPassword()} - Whether the source is password.</li>
- * <li>{@link #isChecked()} - Whether the source is checked.</li>
- * <li>{@link #getItemCount()} - The number of focusable items on the screen.</li>
- * <li>{@link #getCurrentItemIndex()} - The currently focused item index.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
- * <li>{@link #getScrollX()} - The offset of the source left edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getScrollY()} - The offset of the source top edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getFromIndex()} - The zero based index of the first visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getToIndex()} - The zero based index of the last visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getItemCount()} - The total items of the source
- * (for descendants of AdapterView).</li>
* </ul>
* </p>
* <p>
@@ -201,15 +132,11 @@
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #isPassword()} - Whether the source is password.</li>
- * <li>{@link #isChecked()} - Whether the source is checked.</li>
+ * <li>{@link #getText()} - The new text of the source.</li>
+ * <li>{@link #getBeforeText()} - The text of the source before the change.</li>
* <li>{@link #getFromIndex()} - The text change start index.</li>
* <li>{@link #getAddedCount()} - The number of added characters.</li>
* <li>{@link #getRemovedCount()} - The number of removed characters.</li>
- * <li>{@link #getBeforeText()} - The text of the source before the change.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
* </ul>
* </p>
* <p>
@@ -223,13 +150,6 @@
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source.</li>
- * <li>{@link #isPassword()} - Whether the source is password.</li>
- * <li>{@link #getFromIndex()} - The selection start index.</li>
- * <li>{@link #getToIndex()} - The selection end index.</li>
- * <li>{@link #getItemCount()} - The length of the source text.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
* </ul>
* </p>
* <b>View text traversed at movement granularity</b> - represents the event of traversing the
@@ -251,23 +171,11 @@
* <li>{@link #getToIndex()} - The end of the text that was skipped over in this movement.
* This is the ending point when moving forward through the text, but not when moving
* back.</li>
- * <li>{@link #isPassword()} - Whether the source is password.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
- * <li>{@link #getMovementGranularity()} - Sets the granularity at which a view's text
- * was traversed.</li>
* <li>{@link #getAction()} - Gets traversal action which specifies the direction.</li>
* </ul>
* </p>
* <p>
- * <b>View scrolled</b> - represents the event of scrolling a view. If
- * the source is a descendant of {@link android.widget.AdapterView} the
- * scroll is reported in terms of visible items - the first visible item,
- * the last visible item, and the total items - because the the source
- * is unaware of its pixel size since its adapter is responsible for
- * creating views. In all other cases the scroll is reported as the current
- * scroll on the X and Y axis respectively plus the height of the source in
- * pixels.</br>
+ * <b>View scrolled</b> - represents the event of scrolling a view. </br>
* <em>Type:</em> {@link #TYPE_VIEW_SCROLLED}</br>
* <em>Properties:</em></br>
* <ul>
@@ -276,37 +184,19 @@
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source's sub-tree.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
- * <li>{@link #getScrollX()} - The offset of the source left edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getScrollY()} - The offset of the source top edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getFromIndex()} - The zero based index of the first visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getToIndex()} - The zero based index of the last visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getItemCount()} - The total items of the source
- * (for descendants of AdapterView).</li>
+ * <li>{@link #getScrollDeltaX()} - The difference in the horizontal position.</li>
+ * <li>{@link #getScrollDeltaY()} - The difference in the vertical position.</li>
* </ul>
- * <em>Note:</em> This event type is not dispatched to descendants though
- * {@link android.view.View#dispatchPopulateAccessibilityEvent(AccessibilityEvent)
- * View.dispatchPopulateAccessibilityEvent(AccessibilityEvent)}, hence the event
- * source {@link android.view.View} and the sub-tree rooted at it will not receive
- * calls to {@link android.view.View#onPopulateAccessibilityEvent(AccessibilityEvent)
- * View.onPopulateAccessibilityEvent(AccessibilityEvent)}. The preferred way to add
- * text content to such events is by setting the
- * {@link android.R.styleable#View_contentDescription contentDescription} of the source
- * view.</br>
* </p>
* <p>
* <b>TRANSITION TYPES</b></br>
* </p>
* <p>
- * <b>Window state changed</b> - represents the event of opening a
- * {@link android.widget.PopupWindow}, {@link android.view.Menu},
- * {@link android.app.Dialog}, etc.</br>
+ * <b>Window state changed</b> - represents the event of a change to a section of
+ * the user interface that is visually distinct. Should be sent from either the
+ * root view of a window or from a view that is marked as a pane
+ * {@link android.view.View#setAccessibilityPaneTitle(CharSequence)}. Not that changes
+ * to true windows are represented by {@link #TYPE_WINDOWS_CHANGED}.</br>
* <em>Type:</em> {@link #TYPE_WINDOW_STATE_CHANGED}</br>
* <em>Properties:</em></br>
* <ul>
@@ -315,8 +205,7 @@
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source's sub-tree.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
+ * <li>{@link #getText()} - The text of the source's sub-tree, including the pane titles.</li>
* </ul>
* </p>
* <p>
@@ -325,10 +214,6 @@
* a view size, etc.</br>
* </p>
* <p>
- * <strong>Note:</strong> This event is fired only for the window source of the
- * last accessibility event different from {@link #TYPE_NOTIFICATION_STATE_CHANGED}
- * and its purpose is to notify clients that the content of the user interaction
- * window has changed.</br>
* <em>Type:</em> {@link #TYPE_WINDOW_CONTENT_CHANGED}</br>
* <em>Properties:</em></br>
* <ul>
@@ -339,32 +224,26 @@
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
* </ul>
- * <em>Note:</em> This event type is not dispatched to descendants though
- * {@link android.view.View#dispatchPopulateAccessibilityEvent(AccessibilityEvent)
- * View.dispatchPopulateAccessibilityEvent(AccessibilityEvent)}, hence the event
- * source {@link android.view.View} and the sub-tree rooted at it will not receive
- * calls to {@link android.view.View#onPopulateAccessibilityEvent(AccessibilityEvent)
- * View.onPopulateAccessibilityEvent(AccessibilityEvent)}. The preferred way to add
- * text content to such events is by setting the
- * {@link android.R.styleable#View_contentDescription contentDescription} of the source
- * view.</br>
* </p>
* <p>
- * <b>Windows changed</b> - represents the event of changes in the windows shown on
+ * <b>Windows changed</b> - represents a change in the windows shown on
* the screen such as a window appeared, a window disappeared, a window size changed,
- * a window layer changed, etc.</br>
+ * a window layer changed, etc. These events should only come from the system, which is responsible
+ * for managing windows. The list of windows is available from
+ * {@link android.accessibilityservice.AccessibilityService#getWindows()}.
+ * For regions of the user interface that are presented as windows but are
+ * controlled by an app's process, use {@link #TYPE_WINDOW_STATE_CHANGED}.</br>
* <em>Type:</em> {@link #TYPE_WINDOWS_CHANGED}</br>
* <em>Properties:</em></br>
* <ul>
* <li>{@link #getEventType()} - The type of the event.</li>
* <li>{@link #getEventTime()} - The event time.</li>
+ * <li>{@link #getWindowChanges()}</li> - The specific change to the source window
* </ul>
* <em>Note:</em> You can retrieve the {@link AccessibilityWindowInfo} for the window
- * source of the event via {@link AccessibilityEvent#getSource()} to get the source
- * node on which then call {@link AccessibilityNodeInfo#getWindow()
- * AccessibilityNodeInfo.getWindow()} to get the window. Also all windows on the screen can
- * be retrieved by a call to {@link android.accessibilityservice.AccessibilityService#getWindows()
- * android.accessibilityservice.AccessibilityService.getWindows()}.
+ * source of the event by looking through the list returned by
+ * {@link android.accessibilityservice.AccessibilityService#getWindows()} for the window whose ID
+ * matches {@link #getWindowId()}.
* </p>
* <p>
* <b>NOTIFICATION TYPES</b></br>
@@ -402,19 +281,6 @@
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source's sub-tree.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
- * <li>{@link #getScrollX()} - The offset of the source left edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getScrollY()} - The offset of the source top edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getFromIndex()} - The zero based index of the first visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getToIndex()} - The zero based index of the last visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getItemCount()} - The total items of the source
- * (for descendants of AdapterView).</li>
* </ul>
* </p>
* <b>View hover exit</b> - represents the event of stopping to hover
@@ -428,19 +294,6 @@
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source's sub-tree.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
- * <li>{@link #getScrollX()} - The offset of the source left edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getScrollY()} - The offset of the source top edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getFromIndex()} - The zero based index of the first visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getToIndex()} - The zero based index of the last visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getItemCount()} - The total items of the source
- * (for descendants of AdapterView).</li>
* </ul>
* </p>
* <p>
@@ -513,10 +366,10 @@
* <b>MISCELLANEOUS TYPES</b></br>
* </p>
* <p>
- * <b>Announcement</b> - represents the event of an application making an
- * announcement. Usually this announcement is related to some sort of a context
- * change for which none of the events representing UI transitions is a good fit.
- * For example, announcing a new page in a book.</br>
+ * <b>Announcement</b> - represents the event of an application requesting a screen reader to make
+ * an announcement. Because the event carries no semantic meaning, this event is appropriate only
+ * in exceptional situations where additional screen reader output is needed but other types of
+ * accessibility services do not need to be aware of the change.</br>
* <em>Type:</em> {@link #TYPE_ANNOUNCEMENT}</br>
* <em>Properties:</em></br>
* <ul>
@@ -526,7 +379,6 @@
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
* <li>{@link #getText()} - The text of the announcement.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
* </ul>
* </p>
*
@@ -586,8 +438,10 @@
public static final int TYPE_VIEW_TEXT_CHANGED = 0x00000010;
/**
- * Represents the event of opening a {@link android.widget.PopupWindow},
- * {@link android.view.Menu}, {@link android.app.Dialog}, etc.
+ * Represents the event of a change to a visually distinct section of the user interface.
+ * These events should only be dispatched from {@link android.view.View}s that have
+ * accessibility pane titles, and replaces {@link #TYPE_WINDOW_CONTENT_CHANGED} for those
+ * sources. Details about the change are available from {@link #getContentChangeTypes()}.
*/
public static final int TYPE_WINDOW_STATE_CHANGED = 0x00000020;
@@ -674,7 +528,8 @@
public static final int TYPE_TOUCH_INTERACTION_END = 0x00200000;
/**
- * Represents the event change in the windows shown on the screen.
+ * Represents the event change in the system windows shown on the screen. This event type should
+ * only be dispatched by the system.
*/
public static final int TYPE_WINDOWS_CHANGED = 0x00400000;
@@ -696,7 +551,8 @@
/**
* Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
- * A node in the subtree rooted at the source node was added or removed.
+ * One or more content changes occurred in the the subtree rooted at the source node,
+ * or the subtree's structure changed when a node was added or removed.
*/
public static final int CONTENT_CHANGE_TYPE_SUBTREE = 0x00000001;
@@ -712,6 +568,124 @@
*/
public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 0x00000004;
+ /**
+ * Change type for {@link #TYPE_WINDOW_STATE_CHANGED} event:
+ * The node's pane title changed.
+ */
+ public static final int CONTENT_CHANGE_TYPE_PANE_TITLE = 0x00000008;
+
+ /**
+ * Change type for {@link #TYPE_WINDOW_STATE_CHANGED} event:
+ * The node has a pane title, and either just appeared or just was assigned a title when it
+ * had none before.
+ */
+ public static final int CONTENT_CHANGE_TYPE_PANE_APPEARED = 0x00000010;
+
+ /**
+ * Change type for {@link #TYPE_WINDOW_STATE_CHANGED} event:
+ * Can mean one of two slightly different things. The primary meaning is that the node has
+ * a pane title, and was removed from the node hierarchy. It will also be sent if the pane
+ * title is set to {@code null} after it contained a title.
+ * No source will be returned if the node is no longer on the screen. To make the change more
+ * clear for the user, the first entry in {@link #getText()} will return the value that would
+ * have been returned by {@code getSource().getPaneTitle()}.
+ */
+ public static final int CONTENT_CHANGE_TYPE_PANE_DISAPPEARED = 0x00000020;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window was added.
+ */
+ public static final int WINDOWS_CHANGE_ADDED = 0x00000001;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * A window was removed.
+ */
+ public static final int WINDOWS_CHANGE_REMOVED = 0x00000002;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's title changed.
+ */
+ public static final int WINDOWS_CHANGE_TITLE = 0x00000004;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's bounds changed.
+ */
+ public static final int WINDOWS_CHANGE_BOUNDS = 0x00000008;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's layer changed.
+ */
+ public static final int WINDOWS_CHANGE_LAYER = 0x00000010;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's {@link AccessibilityWindowInfo#isActive()} changed.
+ */
+ public static final int WINDOWS_CHANGE_ACTIVE = 0x00000020;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's {@link AccessibilityWindowInfo#isFocused()} changed.
+ */
+ public static final int WINDOWS_CHANGE_FOCUSED = 0x00000040;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's {@link AccessibilityWindowInfo#isAccessibilityFocused()} changed.
+ */
+ public static final int WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED = 0x00000080;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's parent changed.
+ */
+ public static final int WINDOWS_CHANGE_PARENT = 0x00000100;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's children changed.
+ */
+ public static final int WINDOWS_CHANGE_CHILDREN = 0x00000200;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window either entered or exited picture-in-picture mode.
+ */
+ public static final int WINDOWS_CHANGE_PIP = 0x00000400;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "WINDOWS_CHANGE_" }, value = {
+ WINDOWS_CHANGE_ADDED,
+ WINDOWS_CHANGE_REMOVED,
+ WINDOWS_CHANGE_TITLE,
+ WINDOWS_CHANGE_BOUNDS,
+ WINDOWS_CHANGE_LAYER,
+ WINDOWS_CHANGE_ACTIVE,
+ WINDOWS_CHANGE_FOCUSED,
+ WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED,
+ WINDOWS_CHANGE_PARENT,
+ WINDOWS_CHANGE_CHILDREN,
+ WINDOWS_CHANGE_PIP
+ })
+ public @interface WindowsChangeTypes {}
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "CONTENT_CHANGE_TYPE_" },
+ value = {
+ CONTENT_CHANGE_TYPE_UNDEFINED,
+ CONTENT_CHANGE_TYPE_SUBTREE,
+ CONTENT_CHANGE_TYPE_TEXT,
+ CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION,
+ CONTENT_CHANGE_TYPE_PANE_TITLE
+ })
+ public @interface ContentChangeTypes {}
/** @hide */
@IntDef(flag = true, prefix = { "TYPE_" }, value = {
@@ -782,6 +756,7 @@
int mMovementGranularity;
int mAction;
int mContentChangeTypes;
+ int mWindowChangeTypes;
private ArrayList<AccessibilityRecord> mRecords;
@@ -802,6 +777,7 @@
mMovementGranularity = event.mMovementGranularity;
mAction = event.mAction;
mContentChangeTypes = event.mContentChangeTypes;
+ mWindowChangeTypes = event.mWindowChangeTypes;
mEventTime = event.mEventTime;
mPackageName = event.mPackageName;
}
@@ -885,6 +861,7 @@
* <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED}
* </ul>
*/
+ @ContentChangeTypes
public int getContentChangeTypes() {
return mContentChangeTypes;
}
@@ -913,12 +890,49 @@
* @throws IllegalStateException If called from an AccessibilityService.
* @see #getContentChangeTypes()
*/
- public void setContentChangeTypes(int changeTypes) {
+ public void setContentChangeTypes(@ContentChangeTypes int changeTypes) {
enforceNotSealed();
mContentChangeTypes = changeTypes;
}
/**
+ * Get the bit mask of change types signaled by a {@link #TYPE_WINDOWS_CHANGED} event. A
+ * single event may represent multiple change types.
+ *
+ * @return The bit mask of change types.
+ */
+ @WindowsChangeTypes
+ public int getWindowChanges() {
+ return mWindowChangeTypes;
+ }
+
+ /** @hide */
+ public void setWindowChanges(@WindowsChangeTypes int changes) {
+ mWindowChangeTypes = changes;
+ }
+
+ private static String windowChangeTypesToString(@WindowsChangeTypes int types) {
+ return BitUtils.flagsToString(types, AccessibilityEvent::singleWindowChangeTypeToString);
+ }
+
+ private static String singleWindowChangeTypeToString(int type) {
+ switch (type) {
+ case WINDOWS_CHANGE_ADDED: return "WINDOWS_CHANGE_ADDED";
+ case WINDOWS_CHANGE_REMOVED: return "WINDOWS_CHANGE_REMOVED";
+ case WINDOWS_CHANGE_TITLE: return "WINDOWS_CHANGE_TITLE";
+ case WINDOWS_CHANGE_BOUNDS: return "WINDOWS_CHANGE_BOUNDS";
+ case WINDOWS_CHANGE_LAYER: return "WINDOWS_CHANGE_LAYER";
+ case WINDOWS_CHANGE_ACTIVE: return "WINDOWS_CHANGE_ACTIVE";
+ case WINDOWS_CHANGE_FOCUSED: return "WINDOWS_CHANGE_FOCUSED";
+ case WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED:
+ return "WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED";
+ case WINDOWS_CHANGE_PARENT: return "WINDOWS_CHANGE_PARENT";
+ case WINDOWS_CHANGE_CHILDREN: return "WINDOWS_CHANGE_CHILDREN";
+ default: return Integer.toHexString(type);
+ }
+ }
+
+ /**
* Sets the event type.
*
* @param eventType The event type.
@@ -1025,6 +1039,26 @@
}
/**
+ * Convenience method to obtain a {@link #TYPE_WINDOWS_CHANGED} event for a specific window and
+ * change set.
+ *
+ * @param windowId The ID of the window that changed
+ * @param windowChangeTypes The changes to populate
+ * @return An instance of a TYPE_WINDOWS_CHANGED, populated with the requested fields and with
+ * importantForAccessibility set to {@code true}.
+ *
+ * @hide
+ */
+ public static AccessibilityEvent obtainWindowsChangedEvent(
+ int windowId, int windowChangeTypes) {
+ final AccessibilityEvent event = AccessibilityEvent.obtain(TYPE_WINDOWS_CHANGED);
+ event.setWindowId(windowId);
+ event.setWindowChanges(windowChangeTypes);
+ event.setImportantForAccessibility(true);
+ return event;
+ }
+
+ /**
* Returns a cached instance if such is available or a new one is
* instantiated with its type property set.
*
@@ -1099,6 +1133,7 @@
mMovementGranularity = 0;
mAction = 0;
mContentChangeTypes = 0;
+ mWindowChangeTypes = 0;
mPackageName = null;
mEventTime = 0;
if (mRecords != null) {
@@ -1120,6 +1155,7 @@
mMovementGranularity = parcel.readInt();
mAction = parcel.readInt();
mContentChangeTypes = parcel.readInt();
+ mWindowChangeTypes = parcel.readInt();
mPackageName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
mEventTime = parcel.readLong();
mConnectionId = parcel.readInt();
@@ -1178,6 +1214,7 @@
parcel.writeInt(mMovementGranularity);
parcel.writeInt(mAction);
parcel.writeInt(mContentChangeTypes);
+ parcel.writeInt(mWindowChangeTypes);
TextUtils.writeToParcel(mPackageName, parcel, 0);
parcel.writeLong(mEventTime);
parcel.writeInt(mConnectionId);
@@ -1236,41 +1273,33 @@
builder.append("EventType: ").append(eventTypeToString(mEventType));
builder.append("; EventTime: ").append(mEventTime);
builder.append("; PackageName: ").append(mPackageName);
- builder.append("; MovementGranularity: ").append(mMovementGranularity);
- builder.append("; Action: ").append(mAction);
- builder.append(super.toString());
- if (DEBUG) {
- builder.append("\n");
+ if (!DEBUG_CONCISE_TOSTRING || mMovementGranularity != 0) {
+ builder.append("; MovementGranularity: ").append(mMovementGranularity);
+ }
+ if (!DEBUG_CONCISE_TOSTRING || mAction != 0) {
+ builder.append("; Action: ").append(mAction);
+ }
+ if (!DEBUG_CONCISE_TOSTRING || mContentChangeTypes != 0) {
builder.append("; ContentChangeTypes: ").append(
contentChangeTypesToString(mContentChangeTypes));
- builder.append("; sourceWindowId: ").append(mSourceWindowId);
- builder.append("; mSourceNodeId: ").append(mSourceNodeId);
- for (int i = 0; i < getRecordCount(); i++) {
- final AccessibilityRecord record = getRecord(i);
- builder.append(" Record ");
- builder.append(i);
- builder.append(":");
- builder.append(" [ ClassName: " + record.mClassName);
- builder.append("; Text: " + record.mText);
- builder.append("; ContentDescription: " + record.mContentDescription);
- builder.append("; ItemCount: " + record.mItemCount);
- builder.append("; CurrentItemIndex: " + record.mCurrentItemIndex);
- builder.append("; IsEnabled: " + record.isEnabled());
- builder.append("; IsPassword: " + record.isPassword());
- builder.append("; IsChecked: " + record.isChecked());
- builder.append("; IsFullScreen: " + record.isFullScreen());
- builder.append("; Scrollable: " + record.isScrollable());
- builder.append("; BeforeText: " + record.mBeforeText);
- builder.append("; FromIndex: " + record.mFromIndex);
- builder.append("; ToIndex: " + record.mToIndex);
- builder.append("; ScrollX: " + record.mScrollX);
- builder.append("; ScrollY: " + record.mScrollY);
- builder.append("; AddedCount: " + record.mAddedCount);
- builder.append("; RemovedCount: " + record.mRemovedCount);
- builder.append("; ParcelableData: " + record.mParcelableData);
- builder.append(" ]");
+ }
+ if (!DEBUG_CONCISE_TOSTRING || mWindowChangeTypes != 0) {
+ builder.append("; WindowChangeTypes: ").append(
+ contentChangeTypesToString(mWindowChangeTypes));
+ }
+ super.appendTo(builder);
+ if (DEBUG || DEBUG_CONCISE_TOSTRING) {
+ if (!DEBUG_CONCISE_TOSTRING) {
builder.append("\n");
}
+ if (DEBUG) {
+ builder.append("; SourceWindowId: ").append(mSourceWindowId);
+ builder.append("; SourceNodeId: ").append(mSourceNodeId);
+ }
+ for (int i = 0; i < getRecordCount(); i++) {
+ builder.append(" Record ").append(i).append(":");
+ getRecord(i).appendTo(builder).append("\n");
+ }
} else {
builder.append("; recordCount: ").append(getRecordCount());
}
diff --git a/android/view/accessibility/AccessibilityNodeInfo.java b/android/view/accessibility/AccessibilityNodeInfo.java
index 28ef697..23e7d61 100644
--- a/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/android/view/accessibility/AccessibilityNodeInfo.java
@@ -639,6 +639,8 @@
private static final int BOOLEAN_PROPERTY_IS_SHOWING_HINT = 0x0100000;
+ private static final int BOOLEAN_PROPERTY_IS_HEADING = 0x0200000;
+
/**
* Bits that provide the id of a virtual descendant of a view.
*/
@@ -723,7 +725,9 @@
private CharSequence mText;
private CharSequence mHintText;
private CharSequence mError;
+ private CharSequence mPaneTitle;
private CharSequence mContentDescription;
+ private CharSequence mTooltipText;
private String mViewIdResourceName;
private ArrayList<String> mExtraDataKeys;
@@ -2033,6 +2037,33 @@
}
/**
+ * If this node represents a visually distinct region of the screen that may update separately
+ * from the rest of the window, it is considered a pane. Set the pane title to indicate that
+ * the node is a pane, and to provide a title for it.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ * @param paneTitle The title of the pane represented by this node.
+ */
+ public void setPaneTitle(@Nullable CharSequence paneTitle) {
+ enforceNotSealed();
+ mPaneTitle = (paneTitle == null)
+ ? null : paneTitle.subSequence(0, paneTitle.length());
+ }
+
+ /**
+ * Get the title of the pane represented by this node.
+ *
+ * @return The title of the pane represented by this node, or {@code null} if this node does
+ * not represent a pane.
+ */
+ public @Nullable CharSequence getPaneTitle() {
+ return mPaneTitle;
+ }
+
+ /**
* Get the drawing order of the view corresponding it this node.
* <p>
* Drawing order is determined only within the node's parent, so this index is only relative
@@ -2381,6 +2412,30 @@
}
/**
+ * Returns whether node represents a heading.
+ *
+ * @return {@code true} if the node is a heading, {@code false} otherwise.
+ */
+ public boolean isHeading() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_IS_HEADING);
+ }
+
+ /**
+ * Sets whether the node represents a heading.
+ *
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param isHeading {@code true} if the node is a heading, {@code false} otherwise.
+ */
+ public void setHeading(boolean isHeading) {
+ setBooleanProperty(BOOLEAN_PROPERTY_IS_HEADING, isHeading);
+ }
+
+ /**
* Gets the package this node comes from.
*
* @return The package name.
@@ -2601,6 +2656,34 @@
}
/**
+ * Gets the tooltip text of this node.
+ *
+ * @return The tooltip text.
+ */
+ @Nullable
+ public CharSequence getTooltipText() {
+ return mTooltipText;
+ }
+
+ /**
+ * Sets the tooltip text of this node.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param tooltipText The tooltip text.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setTooltipText(@Nullable CharSequence tooltipText) {
+ enforceNotSealed();
+ mTooltipText = (tooltipText == null) ? null
+ : tooltipText.subSequence(0, tooltipText.length());
+ }
+
+ /**
* Sets the view for which the view represented by this info serves as a
* label for accessibility purposes.
*
@@ -3151,6 +3234,14 @@
nonDefaultFields |= bitAt(fieldIndex);
}
fieldIndex++;
+ if (!Objects.equals(mPaneTitle, DEFAULT.mPaneTitle)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (!Objects.equals(mTooltipText, DEFAULT.mTooltipText)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
if (!Objects.equals(mViewIdResourceName, DEFAULT.mViewIdResourceName)) {
nonDefaultFields |= bitAt(fieldIndex);
}
@@ -3270,6 +3361,9 @@
if (isBitSet(nonDefaultFields, fieldIndex++)) {
parcel.writeCharSequence(mContentDescription);
}
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mPaneTitle);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mTooltipText);
+
if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeString(mViewIdResourceName);
if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mTextSelectionStart);
@@ -3341,6 +3435,8 @@
mHintText = other.mHintText;
mError = other.mError;
mContentDescription = other.mContentDescription;
+ mPaneTitle = other.mPaneTitle;
+ mTooltipText = other.mTooltipText;
mViewIdResourceName = other.mViewIdResourceName;
if (mActions != null) mActions.clear();
@@ -3461,6 +3557,8 @@
if (isBitSet(nonDefaultFields, fieldIndex++)) {
mContentDescription = parcel.readCharSequence();
}
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mPaneTitle = parcel.readString();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mTooltipText = parcel.readCharSequence();
if (isBitSet(nonDefaultFields, fieldIndex++)) mViewIdResourceName = parcel.readString();
if (isBitSet(nonDefaultFields, fieldIndex++)) mTextSelectionStart = parcel.readInt();
@@ -3623,6 +3721,10 @@
return "ACTION_SET_PROGRESS";
case R.id.accessibilityActionContextClick:
return "ACTION_CONTEXT_CLICK";
+ case R.id.accessibilityActionShowTooltip:
+ return "ACTION_SHOW_TOOLTIP";
+ case R.id.accessibilityActionHideTooltip:
+ return "ACTION_HIDE_TOOLTIP";
default:
return "ACTION_UNKNOWN";
}
@@ -3736,6 +3838,7 @@
builder.append("; error: ").append(mError);
builder.append("; maxTextLength: ").append(mMaxTextLength);
builder.append("; contentDescription: ").append(mContentDescription);
+ builder.append("; tooltipText: ").append(mTooltipText);
builder.append("; viewIdResName: ").append(mViewIdResourceName);
builder.append("; checkable: ").append(isCheckable());
@@ -4150,6 +4253,20 @@
public static final AccessibilityAction ACTION_MOVE_WINDOW =
new AccessibilityAction(R.id.accessibilityActionMoveWindow);
+ /**
+ * Action to show a tooltip. A node should expose this action only for views with tooltip
+ * text that but are not currently showing a tooltip.
+ */
+ public static final AccessibilityAction ACTION_SHOW_TOOLTIP =
+ new AccessibilityAction(R.id.accessibilityActionShowTooltip);
+
+ /**
+ * Action to hide a tooltip. A node should expose this action only for views that are
+ * currently showing a tooltip.
+ */
+ public static final AccessibilityAction ACTION_HIDE_TOOLTIP =
+ new AccessibilityAction(R.id.accessibilityActionHideTooltip);
+
private final int mActionId;
private final CharSequence mLabel;
@@ -4562,7 +4679,8 @@
* @param rowSpan The number of rows the item spans.
* @param columnIndex The column index at which the item is located.
* @param columnSpan The number of columns the item spans.
- * @param heading Whether the item is a heading.
+ * @param heading Whether the item is a heading. (Prefer
+ * {@link AccessibilityNodeInfo#setHeading(boolean)}).
*/
public static CollectionItemInfo obtain(int rowIndex, int rowSpan,
int columnIndex, int columnSpan, boolean heading) {
@@ -4576,7 +4694,8 @@
* @param rowSpan The number of rows the item spans.
* @param columnIndex The column index at which the item is located.
* @param columnSpan The number of columns the item spans.
- * @param heading Whether the item is a heading.
+ * @param heading Whether the item is a heading. (Prefer
+ * {@link AccessibilityNodeInfo#setHeading(boolean)})
* @param selected Whether the item is selected.
*/
public static CollectionItemInfo obtain(int rowIndex, int rowSpan,
@@ -4663,6 +4782,7 @@
* heading, table header, etc.
*
* @return If the item is a heading.
+ * @deprecated Use {@link AccessibilityNodeInfo#isHeading()}
*/
public boolean isHeading() {
return mHeading;
diff --git a/android/view/accessibility/AccessibilityRecord.java b/android/view/accessibility/AccessibilityRecord.java
index fa505c9..0a709f8 100644
--- a/android/view/accessibility/AccessibilityRecord.java
+++ b/android/view/accessibility/AccessibilityRecord.java
@@ -16,6 +16,8 @@
package android.view.accessibility;
+import static com.android.internal.util.CollectionUtils.isEmpty;
+
import android.annotation.Nullable;
import android.os.Parcelable;
import android.view.View;
@@ -55,6 +57,8 @@
* @see AccessibilityNodeInfo
*/
public class AccessibilityRecord {
+ /** @hide */
+ protected static final boolean DEBUG_CONCISE_TOSTRING = false;
private static final int UNDEFINED = -1;
@@ -888,28 +892,69 @@
@Override
public String toString() {
- StringBuilder builder = new StringBuilder();
- builder.append(" [ ClassName: " + mClassName);
- builder.append("; Text: " + mText);
- builder.append("; ContentDescription: " + mContentDescription);
- builder.append("; ItemCount: " + mItemCount);
- builder.append("; CurrentItemIndex: " + mCurrentItemIndex);
- builder.append("; IsEnabled: " + getBooleanProperty(PROPERTY_ENABLED));
- builder.append("; IsPassword: " + getBooleanProperty(PROPERTY_PASSWORD));
- builder.append("; IsChecked: " + getBooleanProperty(PROPERTY_CHECKED));
- builder.append("; IsFullScreen: " + getBooleanProperty(PROPERTY_FULL_SCREEN));
- builder.append("; Scrollable: " + getBooleanProperty(PROPERTY_SCROLLABLE));
- builder.append("; BeforeText: " + mBeforeText);
- builder.append("; FromIndex: " + mFromIndex);
- builder.append("; ToIndex: " + mToIndex);
- builder.append("; ScrollX: " + mScrollX);
- builder.append("; ScrollY: " + mScrollY);
- builder.append("; MaxScrollX: " + mMaxScrollX);
- builder.append("; MaxScrollY: " + mMaxScrollY);
- builder.append("; AddedCount: " + mAddedCount);
- builder.append("; RemovedCount: " + mRemovedCount);
- builder.append("; ParcelableData: " + mParcelableData);
+ return appendTo(new StringBuilder()).toString();
+ }
+
+ StringBuilder appendTo(StringBuilder builder) {
+ builder.append(" [ ClassName: ").append(mClassName);
+ if (!DEBUG_CONCISE_TOSTRING || !isEmpty(mText)) {
+ appendPropName(builder, "Text").append(mText);
+ }
+ append(builder, "ContentDescription", mContentDescription);
+ append(builder, "ItemCount", mItemCount);
+ append(builder, "CurrentItemIndex", mCurrentItemIndex);
+
+ appendUnless(true, PROPERTY_ENABLED, builder);
+ appendUnless(false, PROPERTY_PASSWORD, builder);
+ appendUnless(false, PROPERTY_CHECKED, builder);
+ appendUnless(false, PROPERTY_FULL_SCREEN, builder);
+ appendUnless(false, PROPERTY_SCROLLABLE, builder);
+
+ append(builder, "BeforeText", mBeforeText);
+ append(builder, "FromIndex", mFromIndex);
+ append(builder, "ToIndex", mToIndex);
+ append(builder, "ScrollX", mScrollX);
+ append(builder, "ScrollY", mScrollY);
+ append(builder, "MaxScrollX", mMaxScrollX);
+ append(builder, "MaxScrollY", mMaxScrollY);
+ append(builder, "AddedCount", mAddedCount);
+ append(builder, "RemovedCount", mRemovedCount);
+ append(builder, "ParcelableData", mParcelableData);
builder.append(" ]");
- return builder.toString();
+ return builder;
+ }
+
+ private void appendUnless(boolean defValue, int prop, StringBuilder builder) {
+ boolean value = getBooleanProperty(prop);
+ if (DEBUG_CONCISE_TOSTRING && value == defValue) return;
+ appendPropName(builder, singleBooleanPropertyToString(prop))
+ .append(value);
+ }
+
+ private static String singleBooleanPropertyToString(int prop) {
+ switch (prop) {
+ case PROPERTY_CHECKED: return "Checked";
+ case PROPERTY_ENABLED: return "Enabled";
+ case PROPERTY_PASSWORD: return "Password";
+ case PROPERTY_FULL_SCREEN: return "FullScreen";
+ case PROPERTY_SCROLLABLE: return "Scrollable";
+ case PROPERTY_IMPORTANT_FOR_ACCESSIBILITY:
+ return "ImportantForAccessibility";
+ default: return Integer.toHexString(prop);
+ }
+ }
+
+ private void append(StringBuilder builder, String propName, int propValue) {
+ if (DEBUG_CONCISE_TOSTRING && propValue == UNDEFINED) return;
+ appendPropName(builder, propName).append(propValue);
+ }
+
+ private void append(StringBuilder builder, String propName, Object propValue) {
+ if (DEBUG_CONCISE_TOSTRING && propValue == null) return;
+ appendPropName(builder, propName).append(propValue);
+ }
+
+ private StringBuilder appendPropName(StringBuilder builder, String propName) {
+ return builder.append("; ").append(propName).append(": ");
}
}
diff --git a/android/view/accessibility/AccessibilityViewHierarchyState.java b/android/view/accessibility/AccessibilityViewHierarchyState.java
new file mode 100644
index 0000000..447fafa
--- /dev/null
+++ b/android/view/accessibility/AccessibilityViewHierarchyState.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017 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.accessibility;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * Accessibility-related state of a {@link android.view.ViewRootImpl}
+ *
+ * @hide
+ */
+public class AccessibilityViewHierarchyState {
+ private @Nullable SendViewScrolledAccessibilityEvent mSendViewScrolledAccessibilityEvent;
+ private @Nullable SendWindowContentChangedAccessibilityEvent
+ mSendWindowContentChangedAccessibilityEvent;
+
+ /**
+ * @return a {@link SendViewScrolledAccessibilityEvent}, creating one if needed
+ */
+ public @NonNull SendViewScrolledAccessibilityEvent getSendViewScrolledAccessibilityEvent() {
+ if (mSendViewScrolledAccessibilityEvent == null) {
+ mSendViewScrolledAccessibilityEvent = new SendViewScrolledAccessibilityEvent();
+ }
+ return mSendViewScrolledAccessibilityEvent;
+ }
+
+ public boolean isScrollEventSenderInitialized() {
+ return mSendViewScrolledAccessibilityEvent != null;
+ }
+
+ /**
+ * @return a {@link SendWindowContentChangedAccessibilityEvent}, creating one if needed
+ */
+ public @NonNull SendWindowContentChangedAccessibilityEvent
+ getSendWindowContentChangedAccessibilityEvent() {
+ if (mSendWindowContentChangedAccessibilityEvent == null) {
+ mSendWindowContentChangedAccessibilityEvent =
+ new SendWindowContentChangedAccessibilityEvent();
+ }
+ return mSendWindowContentChangedAccessibilityEvent;
+ }
+
+ public boolean isWindowContentChangedEventSenderInitialized() {
+ return mSendWindowContentChangedAccessibilityEvent != null;
+ }
+}
diff --git a/android/view/accessibility/AccessibilityWindowInfo.java b/android/view/accessibility/AccessibilityWindowInfo.java
index ef1a3f3..c1c9174 100644
--- a/android/view/accessibility/AccessibilityWindowInfo.java
+++ b/android/view/accessibility/AccessibilityWindowInfo.java
@@ -21,9 +21,12 @@
import android.graphics.Rect;
import android.os.Parcel;
import android.os.Parcelable;
+import android.text.TextUtils;
import android.util.LongArray;
import android.util.Pools.SynchronizedPool;
+import android.view.accessibility.AccessibilityEvent.WindowsChangeTypes;
+import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -575,7 +578,7 @@
StringBuilder builder = new StringBuilder();
builder.append("AccessibilityWindowInfo[");
builder.append("title=").append(mTitle);
- builder.append("id=").append(mId);
+ builder.append(", id=").append(mId);
builder.append(", type=").append(typeToString(mType));
builder.append(", layer=").append(mLayer);
builder.append(", bounds=").append(mBoundsInScreen);
@@ -713,6 +716,60 @@
return false;
}
+ /**
+ * Reports how this window differs from a possibly different state of the same window. The
+ * argument must have the same id and type as neither of those properties may change.
+ *
+ * @param other The new state.
+ * @return A set of flags showing how the window has changes, or 0 if the two states are the
+ * same.
+ *
+ * @hide
+ */
+ @WindowsChangeTypes
+ public int differenceFrom(AccessibilityWindowInfo other) {
+ if (other.mId != mId) {
+ throw new IllegalArgumentException("Not same window.");
+ }
+ if (other.mType != mType) {
+ throw new IllegalArgumentException("Not same type.");
+ }
+ int changes = 0;
+ if (!TextUtils.equals(mTitle, other.mTitle)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_TITLE;
+ }
+
+ if (!mBoundsInScreen.equals(other.mBoundsInScreen)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_BOUNDS;
+ }
+ if (mLayer != other.mLayer) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_LAYER;
+ }
+ if (getBooleanProperty(BOOLEAN_PROPERTY_ACTIVE)
+ != other.getBooleanProperty(BOOLEAN_PROPERTY_ACTIVE)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_ACTIVE;
+ }
+ if (getBooleanProperty(BOOLEAN_PROPERTY_FOCUSED)
+ != other.getBooleanProperty(BOOLEAN_PROPERTY_FOCUSED)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_FOCUSED;
+ }
+ if (getBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED)
+ != other.getBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED;
+ }
+ if (getBooleanProperty(BOOLEAN_PROPERTY_PICTURE_IN_PICTURE)
+ != other.getBooleanProperty(BOOLEAN_PROPERTY_PICTURE_IN_PICTURE)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_PIP;
+ }
+ if (mParentId != other.mParentId) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_PARENT;
+ }
+ if (!Objects.equals(mChildIds, other.mChildIds)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_CHILDREN;
+ }
+ return changes;
+ }
+
public static final Parcelable.Creator<AccessibilityWindowInfo> CREATOR =
new Creator<AccessibilityWindowInfo>() {
@Override
diff --git a/android/view/accessibility/SendViewScrolledAccessibilityEvent.java b/android/view/accessibility/SendViewScrolledAccessibilityEvent.java
new file mode 100644
index 0000000..40a1b6a
--- /dev/null
+++ b/android/view/accessibility/SendViewScrolledAccessibilityEvent.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017 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.accessibility;
+
+
+import android.annotation.NonNull;
+import android.view.View;
+
+/**
+ * Sender for {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} accessibility event.
+ *
+ * @hide
+ */
+public class SendViewScrolledAccessibilityEvent extends ThrottlingAccessibilityEventSender {
+
+ public int mDeltaX;
+ public int mDeltaY;
+
+ /**
+ * Post a scroll event to be sent for the given view
+ */
+ public void post(View source, int dx, int dy) {
+ if (!isPendingFor(source)) sendNowIfPending();
+
+ mDeltaX += dx;
+ mDeltaY += dy;
+
+ if (!isPendingFor(source)) scheduleFor(source);
+ }
+
+ @Override
+ protected void performSendEvent(@NonNull View source) {
+ AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED);
+ event.setScrollDeltaX(mDeltaX);
+ event.setScrollDeltaY(mDeltaY);
+ source.sendAccessibilityEventUnchecked(event);
+ }
+
+ @Override
+ protected void resetState(@NonNull View source) {
+ mDeltaX = 0;
+ mDeltaY = 0;
+ }
+}
diff --git a/android/view/accessibility/SendWindowContentChangedAccessibilityEvent.java b/android/view/accessibility/SendWindowContentChangedAccessibilityEvent.java
new file mode 100644
index 0000000..df38fba
--- /dev/null
+++ b/android/view/accessibility/SendWindowContentChangedAccessibilityEvent.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2017 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.accessibility;
+
+
+import static com.android.internal.util.ObjectUtils.firstNotNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.view.View;
+import android.view.ViewParent;
+
+import java.util.HashSet;
+
+/**
+ * @hide
+ */
+public class SendWindowContentChangedAccessibilityEvent
+ extends ThrottlingAccessibilityEventSender {
+
+ private int mChangeTypes = 0;
+
+ private HashSet<View> mTempHashSet;
+
+ @Override
+ protected void performSendEvent(@NonNull View source) {
+ AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+ event.setContentChangeTypes(mChangeTypes);
+ source.sendAccessibilityEventUnchecked(event);
+ }
+
+ @Override
+ protected void resetState(@Nullable View source) {
+ if (source != null) {
+ source.resetSubtreeAccessibilityStateChanged();
+ }
+ mChangeTypes = 0;
+ }
+
+ /**
+ * Post the {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} event with the given
+ * {@link AccessibilityEvent#getContentChangeTypes change type} for the given view
+ */
+ public void runOrPost(View source, int changeType) {
+ if (source.getAccessibilityLiveRegion() != View.ACCESSIBILITY_LIVE_REGION_NONE) {
+ sendNowIfPending();
+ mChangeTypes = changeType;
+ sendNow(source);
+ } else {
+ mChangeTypes |= changeType;
+ scheduleFor(source);
+ }
+ }
+
+ @Override
+ protected @Nullable View tryMerge(@NonNull View oldSource, @NonNull View newSource) {
+ // If there is no common predecessor, then oldSource points to
+ // a removed view, hence in this case always prefer the newSource.
+ return firstNotNull(
+ getCommonPredecessor(oldSource, newSource),
+ newSource);
+ }
+
+ private View getCommonPredecessor(View first, View second) {
+ if (mTempHashSet == null) {
+ mTempHashSet = new HashSet<>();
+ }
+ HashSet<View> seen = mTempHashSet;
+ seen.clear();
+ View firstCurrent = first;
+ while (firstCurrent != null) {
+ seen.add(firstCurrent);
+ ViewParent firstCurrentParent = firstCurrent.getParent();
+ if (firstCurrentParent instanceof View) {
+ firstCurrent = (View) firstCurrentParent;
+ } else {
+ firstCurrent = null;
+ }
+ }
+ View secondCurrent = second;
+ while (secondCurrent != null) {
+ if (seen.contains(secondCurrent)) {
+ seen.clear();
+ return secondCurrent;
+ }
+ ViewParent secondCurrentParent = secondCurrent.getParent();
+ if (secondCurrentParent instanceof View) {
+ secondCurrent = (View) secondCurrentParent;
+ } else {
+ secondCurrent = null;
+ }
+ }
+ seen.clear();
+ return null;
+ }
+}
diff --git a/android/view/accessibility/ThrottlingAccessibilityEventSender.java b/android/view/accessibility/ThrottlingAccessibilityEventSender.java
new file mode 100644
index 0000000..66fa301
--- /dev/null
+++ b/android/view/accessibility/ThrottlingAccessibilityEventSender.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2017 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.accessibility;
+
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewRootImpl;
+import android.view.ViewRootImpl.CalledFromWrongThreadException;
+
+/**
+ * A throttling {@link AccessibilityEvent} sender that relies on its currently associated
+ * 'source' view's {@link View#postDelayed delayed execution} to delay and possibly
+ * {@link #tryMerge merge} together any events that come in less than
+ * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval
+ * the configured amount of milliseconds} apart.
+ *
+ * The suggested usage is to create a singleton extending this class, holding any state specific to
+ * the particular event type that the subclass represents, and have an 'entrypoint' method that
+ * delegates to {@link #scheduleFor(View)}.
+ * For example:
+ *
+ * {@code
+ * public void post(View view, String text, int resId) {
+ * mText = text;
+ * mId = resId;
+ * scheduleFor(view);
+ * }
+ * }
+ *
+ * @see #scheduleFor(View)
+ * @see #tryMerge(View, View)
+ * @see #performSendEvent(View)
+ * @hide
+ */
+public abstract class ThrottlingAccessibilityEventSender {
+
+ private static final boolean DEBUG = false;
+ private static final String LOG_TAG = "ThrottlingA11ySender";
+
+ View mSource;
+ private long mLastSendTimeMillis = Long.MIN_VALUE;
+ private boolean mIsPending = false;
+
+ private final Runnable mWorker = () -> {
+ View source = mSource;
+ if (DEBUG) Log.d(LOG_TAG, thisClass() + ".run(mSource = " + source + ")");
+
+ if (!checkAndResetIsPending() || source == null) {
+ resetStateInternal();
+ return;
+ }
+
+ // Accessibility may be turned off while we were waiting
+ if (isAccessibilityEnabled(source)) {
+ mLastSendTimeMillis = SystemClock.uptimeMillis();
+ performSendEvent(source);
+ }
+ resetStateInternal();
+ };
+
+ /**
+ * Populate and send an {@link AccessibilityEvent} using the given {@code source} view, as well
+ * as any extra data from this instance's state.
+ *
+ * Send the event via {@link View#sendAccessibilityEventUnchecked(AccessibilityEvent)} or
+ * {@link View#sendAccessibilityEvent(int)} on the provided {@code source} view to allow for
+ * overrides of those methods on {@link View} subclasses to take effect, and/or make sure that
+ * an {@link View#getAccessibilityDelegate() accessibility delegate} is not ignored if any.
+ */
+ protected abstract void performSendEvent(@NonNull View source);
+
+ /**
+ * Perform optional cleanup after {@link #performSendEvent}
+ *
+ * @param source the view this event was associated with
+ */
+ protected abstract void resetState(@Nullable View source);
+
+ /**
+ * Attempt to merge the pending events for source views {@code oldSource} and {@code newSource}
+ * into one, with source set to the resulting {@link View}
+ *
+ * A result of {@code null} means merger is not possible, resulting in the currently pending
+ * event being flushed before proceeding.
+ */
+ protected @Nullable View tryMerge(@NonNull View oldSource, @NonNull View newSource) {
+ return null;
+ }
+
+ /**
+ * Schedules a {@link #performSendEvent} with the source {@link View} set to given
+ * {@code source}
+ *
+ * If an event is already scheduled a {@link #tryMerge merge} will be attempted.
+ * If merging is not possible (as indicated by the null result from {@link #tryMerge}),
+ * the currently scheduled event will be {@link #sendNow sent immediately} and the new one
+ * will be scheduled afterwards.
+ */
+ protected final void scheduleFor(@NonNull View source) {
+ if (DEBUG) Log.d(LOG_TAG, thisClass() + ".scheduleFor(source = " + source + ")");
+
+ Handler uiHandler = source.getHandler();
+ if (uiHandler == null || uiHandler.getLooper() != Looper.myLooper()) {
+ CalledFromWrongThreadException e = new CalledFromWrongThreadException(
+ "Expected to be called from main thread but was called from "
+ + Thread.currentThread());
+ // TODO: Throw the exception
+ Log.e(LOG_TAG, "Accessibility content change on non-UI thread. Future Android "
+ + "versions will throw an exception.", e);
+ }
+
+ if (!isAccessibilityEnabled(source)) return;
+
+ if (mIsPending) {
+ View merged = tryMerge(mSource, source);
+ if (merged != null) {
+ setSource(merged);
+ return;
+ } else {
+ sendNow();
+ }
+ }
+
+ setSource(source);
+
+ final long timeSinceLastMillis = SystemClock.uptimeMillis() - mLastSendTimeMillis;
+ final long minEventIntervalMillis =
+ ViewConfiguration.getSendRecurringAccessibilityEventsInterval();
+ if (timeSinceLastMillis >= minEventIntervalMillis) {
+ sendNow();
+ } else {
+ mSource.postDelayed(mWorker, minEventIntervalMillis - timeSinceLastMillis);
+ }
+ }
+
+ static boolean isAccessibilityEnabled(@NonNull View contextProvider) {
+ return AccessibilityManager.getInstance(contextProvider.getContext()).isEnabled();
+ }
+
+ protected final void sendNow(View source) {
+ setSource(source);
+ sendNow();
+ }
+
+ private void sendNow() {
+ mSource.removeCallbacks(mWorker);
+ mWorker.run();
+ }
+
+ /**
+ * Flush the event if one is pending
+ */
+ public void sendNowIfPending() {
+ if (mIsPending) sendNow();
+ }
+
+ /**
+ * Cancel the event if one is pending and is for the given view
+ */
+ public final void cancelIfPendingFor(@NonNull View source) {
+ if (isPendingFor(source)) cancelIfPending(this);
+ }
+
+ /**
+ * @return whether an event is currently pending for the given source view
+ */
+ protected final boolean isPendingFor(@Nullable View source) {
+ return mIsPending && mSource == source;
+ }
+
+ /**
+ * Cancel the event if one is not null and pending
+ */
+ public static void cancelIfPending(@Nullable ThrottlingAccessibilityEventSender sender) {
+ if (sender == null || !sender.checkAndResetIsPending()) return;
+ sender.mSource.removeCallbacks(sender.mWorker);
+ sender.resetStateInternal();
+ }
+
+ void resetStateInternal() {
+ if (DEBUG) Log.d(LOG_TAG, thisClass() + ".resetStateInternal()");
+
+ resetState(mSource);
+ setSource(null);
+ }
+
+ boolean checkAndResetIsPending() {
+ if (mIsPending) {
+ mIsPending = false;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void setSource(@Nullable View source) {
+ if (DEBUG) Log.d(LOG_TAG, thisClass() + ".setSource(" + source + ")");
+
+ if (source == null && mIsPending) {
+ Log.e(LOG_TAG, "mSource nullified while callback still pending: " + this);
+ return;
+ }
+
+ if (source != null && !mIsPending) {
+ // At most one can be pending at any given time
+ View oldSource = mSource;
+ if (oldSource != null) {
+ ViewRootImpl viewRootImpl = oldSource.getViewRootImpl();
+ if (viewRootImpl != null) {
+ viewRootImpl.flushPendingAccessibilityEvents();
+ }
+ }
+ mIsPending = true;
+ }
+ mSource = source;
+ }
+
+ String thisClass() {
+ return getClass().getSimpleName();
+ }
+
+ @Override
+ public String toString() {
+ return thisClass() + "(" + mSource + ")";
+ }
+
+}
diff --git a/android/view/animation/AnimationUtils.java b/android/view/animation/AnimationUtils.java
index f5c3613..990fbdb 100644
--- a/android/view/animation/AnimationUtils.java
+++ b/android/view/animation/AnimationUtils.java
@@ -156,6 +156,8 @@
anim = new RotateAnimation(c, attrs);
} else if (name.equals("translate")) {
anim = new TranslateAnimation(c, attrs);
+ } else if (name.equals("cliprect")) {
+ anim = new ClipRectAnimation(c, attrs);
} else {
throw new RuntimeException("Unknown animation name: " + parser.getName());
}
diff --git a/android/view/animation/ClipRectAnimation.java b/android/view/animation/ClipRectAnimation.java
index e194927..21509d3 100644
--- a/android/view/animation/ClipRectAnimation.java
+++ b/android/view/animation/ClipRectAnimation.java
@@ -16,7 +16,11 @@
package android.view.animation;
+import android.content.Context;
+import android.content.res.TypedArray;
import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
/**
* An animation that controls the clip of an object. See the
@@ -26,8 +30,84 @@
* @hide
*/
public class ClipRectAnimation extends Animation {
- protected Rect mFromRect = new Rect();
- protected Rect mToRect = new Rect();
+ protected final Rect mFromRect = new Rect();
+ protected final Rect mToRect = new Rect();
+
+ private int mFromLeftType = ABSOLUTE;
+ private int mFromTopType = ABSOLUTE;
+ private int mFromRightType = ABSOLUTE;
+ private int mFromBottomType = ABSOLUTE;
+
+ private int mToLeftType = ABSOLUTE;
+ private int mToTopType = ABSOLUTE;
+ private int mToRightType = ABSOLUTE;
+ private int mToBottomType = ABSOLUTE;
+
+ private float mFromLeftValue;
+ private float mFromTopValue;
+ private float mFromRightValue;
+ private float mFromBottomValue;
+
+ private float mToLeftValue;
+ private float mToTopValue;
+ private float mToRightValue;
+ private float mToBottomValue;
+
+ /**
+ * Constructor used when a ClipRectAnimation is loaded from a resource.
+ *
+ * @param context Application context to use
+ * @param attrs Attribute set from which to read values
+ */
+ public ClipRectAnimation(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.ClipRectAnimation);
+
+ Description d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_fromLeft));
+ mFromLeftType = d.type;
+ mFromLeftValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_fromTop));
+ mFromTopType = d.type;
+ mFromTopValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_fromRight));
+ mFromRightType = d.type;
+ mFromRightValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_fromBottom));
+ mFromBottomType = d.type;
+ mFromBottomValue = d.value;
+
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_toLeft));
+ mToLeftType = d.type;
+ mToLeftValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_toTop));
+ mToTopType = d.type;
+ mToTopValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_toRight));
+ mToRightType = d.type;
+ mToRightValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_toBottom));
+ mToBottomType = d.type;
+ mToBottomValue = d.value;
+
+ a.recycle();
+ }
/**
* Constructor to use when building a ClipRectAnimation from code
@@ -39,8 +119,15 @@
if (fromClip == null || toClip == null) {
throw new RuntimeException("Expected non-null animation clip rects");
}
- mFromRect.set(fromClip);
- mToRect.set(toClip);
+ mFromLeftValue = fromClip.left;
+ mFromTopValue = fromClip.top;
+ mFromRightValue= fromClip.right;
+ mFromBottomValue = fromClip.bottom;
+
+ mToLeftValue = toClip.left;
+ mToTopValue = toClip.top;
+ mToRightValue= toClip.right;
+ mToBottomValue = toClip.bottom;
}
/**
@@ -48,8 +135,7 @@
*/
public ClipRectAnimation(int fromL, int fromT, int fromR, int fromB,
int toL, int toT, int toR, int toB) {
- mFromRect.set(fromL, fromT, fromR, fromB);
- mToRect.set(toL, toT, toR, toB);
+ this(new Rect(fromL, fromT, fromR, fromB), new Rect(toL, toT, toR, toB));
}
@Override
@@ -65,4 +151,17 @@
public boolean willChangeTransformationMatrix() {
return false;
}
+
+ @Override
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ super.initialize(width, height, parentWidth, parentHeight);
+ mFromRect.set((int) resolveSize(mFromLeftType, mFromLeftValue, width, parentWidth),
+ (int) resolveSize(mFromTopType, mFromTopValue, height, parentHeight),
+ (int) resolveSize(mFromRightType, mFromRightValue, width, parentWidth),
+ (int) resolveSize(mFromBottomType, mFromBottomValue, height, parentHeight));
+ mToRect.set((int) resolveSize(mToLeftType, mToLeftValue, width, parentWidth),
+ (int) resolveSize(mToTopType, mToTopValue, height, parentHeight),
+ (int) resolveSize(mToRightType, mToRightValue, width, parentWidth),
+ (int) resolveSize(mToBottomType, mToBottomValue, height, parentHeight));
+ }
}
diff --git a/android/view/autofill/AutofillManager.java b/android/view/autofill/AutofillManager.java
index 2697454..4b24a71 100644
--- a/android/view/autofill/AutofillManager.java
+++ b/android/view/autofill/AutofillManager.java
@@ -53,6 +53,8 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -168,7 +170,6 @@
public static final String EXTRA_CLIENT_STATE =
"android.view.autofill.extra.CLIENT_STATE";
-
/** @hide */
public static final String EXTRA_RESTORE_SESSION_TOKEN =
"android.view.autofill.extra.RESTORE_SESSION_TOKEN";
@@ -258,6 +259,12 @@
public static final int STATE_DISABLED_BY_SERVICE = 4;
/**
+ * Timeout in ms for calls to the field classification service.
+ * @hide
+ */
+ public static final int FC_SERVICE_TIMEOUT = 5000;
+
+ /**
* Makes an authentication id from a request id and a dataset id.
*
* @param requestId The request id.
@@ -340,6 +347,10 @@
@GuardedBy("mLock")
@Nullable private AutofillId mSaveTriggerId;
+ /** set to true when onInvisibleForAutofill is called, used by onAuthenticationResult */
+ @GuardedBy("mLock")
+ private boolean mOnInvisibleCalled;
+
/** If set, session is commited when the activity is finished; otherwise session is canceled. */
@GuardedBy("mLock")
private boolean mSaveOnFinish;
@@ -396,6 +407,11 @@
boolean isVisibleForAutofill();
/**
+ * Client might disable enter/exit event e.g. when activity is paused.
+ */
+ boolean isDisablingEnterExitEventForAutofill();
+
+ /**
* Finds views by traversing the hierarchies of the client.
*
* @param viewIds The autofill ids of the views to find
@@ -498,6 +514,19 @@
}
/**
+ * Called once the client becomes invisible.
+ *
+ * @see AutofillClient#isVisibleForAutofill()
+ *
+ * {@hide}
+ */
+ public void onInvisibleForAutofill() {
+ synchronized (mLock) {
+ mOnInvisibleCalled = true;
+ }
+ }
+
+ /**
* Save state before activity lifecycle
*
* @param outState Place to store the state
@@ -622,21 +651,45 @@
return false;
}
+ private boolean isClientVisibleForAutofillLocked() {
+ final AutofillClient client = getClient();
+ return client != null && client.isVisibleForAutofill();
+ }
+
+ private boolean isClientDisablingEnterExitEvent() {
+ final AutofillClient client = getClient();
+ return client != null && client.isDisablingEnterExitEventForAutofill();
+ }
+
private void notifyViewEntered(@NonNull View view, int flags) {
if (!hasAutofillFeature()) {
return;
}
- AutofillCallback callback = null;
+ AutofillCallback callback;
synchronized (mLock) {
- if (shouldIgnoreViewEnteredLocked(view, flags)) return;
+ callback = notifyViewEnteredLocked(view, flags);
+ }
- ensureServiceClientAddedIfNeededLocked();
+ if (callback != null) {
+ mCallback.onAutofillEvent(view, AutofillCallback.EVENT_INPUT_UNAVAILABLE);
+ }
+ }
- if (!mEnabled) {
- if (mCallback != null) {
- callback = mCallback;
- }
- } else {
+ /** Returns AutofillCallback if need fire EVENT_INPUT_UNAVAILABLE */
+ private AutofillCallback notifyViewEnteredLocked(@NonNull View view, int flags) {
+ if (shouldIgnoreViewEnteredLocked(view, flags)) return null;
+
+ AutofillCallback callback = null;
+
+ ensureServiceClientAddedIfNeededLocked();
+
+ if (!mEnabled) {
+ if (mCallback != null) {
+ callback = mCallback;
+ }
+ } else {
+ // don't notify entered when Activity is already in background
+ if (!isClientDisablingEnterExitEvent()) {
final AutofillId id = getAutofillId(view);
final AutofillValue value = view.getAutofillValue();
@@ -649,10 +702,7 @@
}
}
}
-
- if (callback != null) {
- mCallback.onAutofillEvent(view, AutofillCallback.EVENT_INPUT_UNAVAILABLE);
- }
+ return callback;
}
/**
@@ -665,9 +715,16 @@
return;
}
synchronized (mLock) {
- ensureServiceClientAddedIfNeededLocked();
+ notifyViewExitedLocked(view);
+ }
+ }
- if (mEnabled && isActiveLocked()) {
+ void notifyViewExitedLocked(@NonNull View view) {
+ ensureServiceClientAddedIfNeededLocked();
+
+ if (mEnabled && isActiveLocked()) {
+ // dont notify exited when Activity is already in background
+ if (!isClientDisablingEnterExitEvent()) {
final AutofillId id = getAutofillId(view);
// Update focus on existing session.
@@ -718,7 +775,7 @@
}
}
if (mTrackedViews != null) {
- mTrackedViews.notifyViewVisibilityChanged(id, isVisible);
+ mTrackedViews.notifyViewVisibilityChangedLocked(id, isVisible);
}
}
}
@@ -751,17 +808,32 @@
if (!hasAutofillFeature()) {
return;
}
- AutofillCallback callback = null;
+ AutofillCallback callback;
synchronized (mLock) {
- if (shouldIgnoreViewEnteredLocked(view, flags)) return;
+ callback = notifyViewEnteredLocked(view, virtualId, bounds, flags);
+ }
- ensureServiceClientAddedIfNeededLocked();
+ if (callback != null) {
+ callback.onAutofillEvent(view, virtualId,
+ AutofillCallback.EVENT_INPUT_UNAVAILABLE);
+ }
+ }
- if (!mEnabled) {
- if (mCallback != null) {
- callback = mCallback;
- }
- } else {
+ /** Returns AutofillCallback if need fire EVENT_INPUT_UNAVAILABLE */
+ private AutofillCallback notifyViewEnteredLocked(View view, int virtualId, Rect bounds,
+ int flags) {
+ AutofillCallback callback = null;
+ if (shouldIgnoreViewEnteredLocked(view, flags)) return callback;
+
+ ensureServiceClientAddedIfNeededLocked();
+
+ if (!mEnabled) {
+ if (mCallback != null) {
+ callback = mCallback;
+ }
+ } else {
+ // don't notify entered when Activity is already in background
+ if (!isClientDisablingEnterExitEvent()) {
final AutofillId id = getAutofillId(view, virtualId);
if (!isActiveLocked()) {
@@ -773,11 +845,7 @@
}
}
}
-
- if (callback != null) {
- callback.onAutofillEvent(view, virtualId,
- AutofillCallback.EVENT_INPUT_UNAVAILABLE);
- }
+ return callback;
}
/**
@@ -791,9 +859,16 @@
return;
}
synchronized (mLock) {
- ensureServiceClientAddedIfNeededLocked();
+ notifyViewExitedLocked(view, virtualId);
+ }
+ }
- if (mEnabled && isActiveLocked()) {
+ private void notifyViewExitedLocked(@NonNull View view, int virtualId) {
+ ensureServiceClientAddedIfNeededLocked();
+
+ if (mEnabled && isActiveLocked()) {
+ // don't notify exited when Activity is already in background
+ if (!isClientDisablingEnterExitEvent()) {
final AutofillId id = getAutofillId(view, virtualId);
// Update focus on existing session.
@@ -1027,7 +1102,9 @@
* Gets the user data used for
* <a href="AutofillService.html#FieldClassification">field classification</a>.
*
- * <p><b>Note:</b> This method should only be called by an app providing an autofill service.
+ * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
+ * and it's ignored if the caller currently doesn't have an enabled autofill service for
+ * the user.
*
* @return value previously set by {@link #setUserData(UserData)} or {@code null} if it was
* reset or if the caller currently does not have an enabled autofill service for the user.
@@ -1079,6 +1156,47 @@
}
/**
+ * Gets the name of the default algorithm used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>.
+ *
+ * <p>The default algorithm is used when the algorithm on {@link UserData} is invalid or not
+ * set.
+ *
+ * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
+ * and it's ignored if the caller currently doesn't have an enabled autofill service for
+ * the user.
+ */
+ @Nullable
+ public String getDefaultFieldClassificationAlgorithm() {
+ try {
+ return mService.getDefaultFieldClassificationAlgorithm();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ return null;
+ }
+ }
+
+ /**
+ * Gets the name of all algorithms currently available for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>.
+ *
+ * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
+ * and it returns an empty list if the caller currently doesn't have an enabled autofill service
+ * for the user.
+ */
+ @NonNull
+ public List<String> getAvailableFieldClassificationAlgorithms() {
+ final String[] algorithms;
+ try {
+ algorithms = mService.getAvailableFieldClassificationAlgorithms();
+ return algorithms != null ? Arrays.asList(algorithms) : Collections.emptyList();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ return null;
+ }
+ }
+
+ /**
* Returns {@code true} if autofill is supported by the current device and
* is supported for this user.
*
@@ -1109,7 +1227,7 @@
}
/** @hide */
- public void onAuthenticationResult(int authenticationId, Intent data) {
+ public void onAuthenticationResult(int authenticationId, Intent data, View focusView) {
if (!hasAutofillFeature()) {
return;
}
@@ -1121,9 +1239,24 @@
if (sDebug) Log.d(TAG, "onAuthenticationResult(): d=" + data);
synchronized (mLock) {
- if (!isActiveLocked() || data == null) {
+ if (!isActiveLocked()) {
return;
}
+ // If authenticate activity closes itself during onCreate(), there is no onStop/onStart
+ // of app activity. We enforce enter event to re-show fill ui in such case.
+ // CTS example:
+ // LoginActivityTest#testDatasetAuthTwoFieldsUserCancelsFirstAttempt
+ // LoginActivityTest#testFillResponseAuthBothFieldsUserCancelsFirstAttempt
+ if (!mOnInvisibleCalled && focusView != null
+ && focusView.canNotifyAutofillEnterExitEvent()) {
+ notifyViewExitedLocked(focusView);
+ notifyViewEnteredLocked(focusView, 0);
+ }
+ if (data == null) {
+ // data is set to null when result is not RESULT_OK
+ return;
+ }
+
final Parcelable result = data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT);
final Bundle responseData = new Bundle();
responseData.putParcelable(EXTRA_AUTHENTICATION_RESULT, result);
@@ -1356,6 +1489,9 @@
if (sessionId == mSessionId) {
final AutofillClient client = getClient();
if (client != null) {
+ // clear mOnInvisibleCalled and we will see if receive onInvisibleForAutofill()
+ // before onAuthenticationResult()
+ mOnInvisibleCalled = false;
client.autofillCallbackAuthenticate(authenticationId, intent, fillInIntent);
}
}
@@ -1721,6 +1857,7 @@
pw.print(pfx); pw.print("enabled: "); pw.println(mEnabled);
pw.print(pfx); pw.print("hasService: "); pw.println(mService != null);
pw.print(pfx); pw.print("hasCallback: "); pw.println(mCallback != null);
+ pw.print(pfx); pw.print("onInvisibleCalled "); pw.println(mOnInvisibleCalled);
pw.print(pfx); pw.print("last autofilled data: "); pw.println(mLastAutofilledData);
pw.print(pfx); pw.print("tracked views: ");
if (mTrackedViews == null) {
@@ -1891,15 +2028,13 @@
* @param id the id of the view/virtual view whose visibility changed.
* @param isVisible visible if the view is visible in the view hierarchy.
*/
- void notifyViewVisibilityChanged(@NonNull AutofillId id, boolean isVisible) {
- AutofillClient client = getClient();
-
+ void notifyViewVisibilityChangedLocked(@NonNull AutofillId id, boolean isVisible) {
if (sDebug) {
Log.d(TAG, "notifyViewVisibilityChanged(): id=" + id + " isVisible="
+ isVisible);
}
- if (client != null && client.isVisibleForAutofill()) {
+ if (isClientVisibleForAutofillLocked()) {
if (isVisible) {
if (isInSet(mInvisibleTrackedIds, id)) {
mInvisibleTrackedIds = removeFromSet(mInvisibleTrackedIds, id);
diff --git a/android/view/autofill/AutofillPopupWindow.java b/android/view/autofill/AutofillPopupWindow.java
index 5cba21e..e80fdd9 100644
--- a/android/view/autofill/AutofillPopupWindow.java
+++ b/android/view/autofill/AutofillPopupWindow.java
@@ -78,8 +78,10 @@
public AutofillPopupWindow(@NonNull IAutofillWindowPresenter presenter) {
mWindowPresenter = new WindowPresenter(presenter);
+ setTouchModal(false);
setOutsideTouchable(true);
- setInputMethodMode(INPUT_METHOD_NEEDED);
+ setInputMethodMode(INPUT_METHOD_NOT_NEEDED);
+ setFocusable(true);
}
@Override
diff --git a/android/view/inputmethod/ExtractedText.java b/android/view/inputmethod/ExtractedText.java
index 003f221..1eb300e 100644
--- a/android/view/inputmethod/ExtractedText.java
+++ b/android/view/inputmethod/ExtractedText.java
@@ -29,6 +29,8 @@
public class ExtractedText implements Parcelable {
/**
* The text that has been extracted.
+ *
+ * @see android.widget.TextView#getText()
*/
public CharSequence text;
@@ -88,6 +90,8 @@
/**
* The hint that has been extracted.
+ *
+ * @see android.widget.TextView#getHint()
*/
public CharSequence hint;
diff --git a/android/view/inputmethod/InputConnection.java b/android/view/inputmethod/InputConnection.java
index 57f9895..eba9176 100644
--- a/android/view/inputmethod/InputConnection.java
+++ b/android/view/inputmethod/InputConnection.java
@@ -1,17 +1,17 @@
/*
- * Copyright (C) 2007-2008 The Android Open Source Project
+ * Copyright (C) 2007 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
+ * 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
+ * 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.
+ * 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.inputmethod;
@@ -21,6 +21,7 @@
import android.inputmethodservice.InputMethodService;
import android.os.Bundle;
import android.os.Handler;
+import android.os.LocaleList;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
@@ -131,13 +132,13 @@
* spans. <strong>Editor authors</strong>: you should strive to
* send text with styles if possible, but it is not required.
*/
- static final int GET_TEXT_WITH_STYLES = 0x0001;
+ int GET_TEXT_WITH_STYLES = 0x0001;
/**
* Flag for use with {@link #getExtractedText} to indicate you
* would like to receive updates when the extracted text changes.
*/
- public static final int GET_EXTRACTED_TEXT_MONITOR = 0x0001;
+ int GET_EXTRACTED_TEXT_MONITOR = 0x0001;
/**
* Get <var>n</var> characters of text before the current cursor
@@ -176,7 +177,7 @@
* @return the text before the cursor position; the length of the
* returned text might be less than <var>n</var>.
*/
- public CharSequence getTextBeforeCursor(int n, int flags);
+ CharSequence getTextBeforeCursor(int n, int flags);
/**
* Get <var>n</var> characters of text after the current cursor
@@ -215,7 +216,7 @@
* @return the text after the cursor position; the length of the
* returned text might be less than <var>n</var>.
*/
- public CharSequence getTextAfterCursor(int n, int flags);
+ CharSequence getTextAfterCursor(int n, int flags);
/**
* Gets the selected text, if any.
@@ -249,7 +250,7 @@
* later, returns false when the target application does not implement
* this method.
*/
- public CharSequence getSelectedText(int flags);
+ CharSequence getSelectedText(int flags);
/**
* Retrieve the current capitalization mode in effect at the
@@ -279,7 +280,7 @@
* @return the caps mode flags that are in effect at the current
* cursor position. See TYPE_TEXT_FLAG_CAPS_* in {@link android.text.InputType}.
*/
- public int getCursorCapsMode(int reqModes);
+ int getCursorCapsMode(int reqModes);
/**
* Retrieve the current text in the input connection's editor, and
@@ -314,8 +315,7 @@
* longer valid of the editor can't comply with the request for
* some reason.
*/
- public ExtractedText getExtractedText(ExtractedTextRequest request,
- int flags);
+ ExtractedText getExtractedText(ExtractedTextRequest request, int flags);
/**
* Delete <var>beforeLength</var> characters of text before the
@@ -342,8 +342,8 @@
* delete more characters than are in the editor, as that may have
* ill effects on the application. Calling this method will cause
* the editor to call
- * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int, int, int)}
- * on your service after the batch input is over.</p>
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on your service after the batch input is over.</p>
*
* <p><strong>Editor authors:</strong> please be careful of race
* conditions in implementing this call. An IME can make a change
@@ -369,7 +369,7 @@
* that range.
* @return true on success, false if the input connection is no longer valid.
*/
- public boolean deleteSurroundingText(int beforeLength, int afterLength);
+ boolean deleteSurroundingText(int beforeLength, int afterLength);
/**
* A variant of {@link #deleteSurroundingText(int, int)}. Major differences are:
@@ -397,7 +397,7 @@
* @return true on success, false if the input connection is no longer valid. Returns
* {@code false} when the target application does not implement this method.
*/
- public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength);
+ boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength);
/**
* Replace the currently composing text with the given text, and
@@ -416,8 +416,8 @@
* <p>This is usually called by IMEs to add or remove or change
* characters in the composing span. Calling this method will
* cause the editor to call
- * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int, int, int)}
- * on the current IME after the batch input is over.</p>
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on the current IME after the batch input is over.</p>
*
* <p><strong>Editor authors:</strong> please keep in mind the
* text may be very similar or completely different than what was
@@ -455,7 +455,7 @@
* @return true on success, false if the input connection is no longer
* valid.
*/
- public boolean setComposingText(CharSequence text, int newCursorPosition);
+ boolean setComposingText(CharSequence text, int newCursorPosition);
/**
* Mark a certain region of text as composing text. If there was a
@@ -474,8 +474,8 @@
* <p>Since this does not change the contents of the text, editors should not call
* {@link InputMethodManager#updateSelection(View, int, int, int, int)} and
* IMEs should not receive
- * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int, int, int)}.
- * </p>
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)}.</p>
*
* <p>This has no impact on the cursor/selection position. It may
* result in the cursor being anywhere inside or outside the
@@ -488,7 +488,7 @@
* valid. In {@link android.os.Build.VERSION_CODES#N} and later, false is returned when the
* target application does not implement this method.
*/
- public boolean setComposingRegion(int start, int end);
+ boolean setComposingRegion(int start, int end);
/**
* Have the text editor finish whatever composing text is
@@ -507,7 +507,7 @@
* @return true on success, false if the input connection
* is no longer valid.
*/
- public boolean finishComposingText();
+ boolean finishComposingText();
/**
* Commit text to the text box and set the new cursor position.
@@ -522,8 +522,8 @@
* then {@link #finishComposingText()}.</p>
*
* <p>Calling this method will cause the editor to call
- * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int, int, int)}
- * on the current IME after the batch input is over.
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on the current IME after the batch input is over.
* <strong>Editor authors</strong>, for this to happen you need to
* make the changes known to the input method by calling
* {@link InputMethodManager#updateSelection(View, int, int, int, int)},
@@ -543,7 +543,7 @@
* @return true on success, false if the input connection is no longer
* valid.
*/
- public boolean commitText(CharSequence text, int newCursorPosition);
+ boolean commitText(CharSequence text, int newCursorPosition);
/**
* Commit a completion the user has selected from the possible ones
@@ -569,8 +569,8 @@
*
* <p>Calling this method (with a valid {@link CompletionInfo} object)
* will cause the editor to call
- * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int, int, int)}
- * on the current IME after the batch input is over.
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on the current IME after the batch input is over.
* <strong>Editor authors</strong>, for this to happen you need to
* make the changes known to the input method by calling
* {@link InputMethodManager#updateSelection(View, int, int, int, int)},
@@ -581,15 +581,15 @@
* @return true on success, false if the input connection is no longer
* valid.
*/
- public boolean commitCompletion(CompletionInfo text);
+ boolean commitCompletion(CompletionInfo text);
/**
* Commit a correction automatically performed on the raw user's input. A
* typical example would be to correct typos using a dictionary.
*
* <p>Calling this method will cause the editor to call
- * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int, int, int)}
- * on the current IME after the batch input is over.
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on the current IME after the batch input is over.
* <strong>Editor authors</strong>, for this to happen you need to
* make the changes known to the input method by calling
* {@link InputMethodManager#updateSelection(View, int, int, int, int)},
@@ -601,7 +601,7 @@
* In {@link android.os.Build.VERSION_CODES#N} and later, returns false
* when the target application does not implement this method.
*/
- public boolean commitCorrection(CorrectionInfo correctionInfo);
+ boolean commitCorrection(CorrectionInfo correctionInfo);
/**
* Set the selection of the text editor. To set the cursor
@@ -609,8 +609,8 @@
*
* <p>Since this moves the cursor, calling this method will cause
* the editor to call
- * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int, int, int)}
- * on the current IME after the batch input is over.
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on the current IME after the batch input is over.
* <strong>Editor authors</strong>, for this to happen you need to
* make the changes known to the input method by calling
* {@link InputMethodManager#updateSelection(View, int, int, int, int)},
@@ -628,7 +628,7 @@
* @return true on success, false if the input connection is no longer
* valid.
*/
- public boolean setSelection(int start, int end);
+ boolean setSelection(int start, int end);
/**
* Have the editor perform an action it has said it can do.
@@ -642,7 +642,7 @@
* @return true on success, false if the input connection is no longer
* valid.
*/
- public boolean performEditorAction(int editorAction);
+ boolean performEditorAction(int editorAction);
/**
* Perform a context menu action on the field. The given id may be one of:
@@ -652,7 +652,7 @@
* {@link android.R.id#paste}, {@link android.R.id#copyUrl},
* or {@link android.R.id#switchInputMethod}
*/
- public boolean performContextMenuAction(int id);
+ boolean performContextMenuAction(int id);
/**
* Tell the editor that you are starting a batch of editor
@@ -662,8 +662,8 @@
*
* <p><strong>IME authors:</strong> use this to avoid getting
* calls to
- * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int, int, int)}
- * corresponding to intermediate state. Also, use this to avoid
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} corresponding to intermediate state. Also, use this to avoid
* flickers that may arise from displaying intermediate state. Be
* sure to call {@link #endBatchEdit} for each call to this, or
* you may block updates in the editor.</p>
@@ -678,7 +678,7 @@
* this method starts a batch edit, that means it will always return true
* unless the input connection is no longer valid.
*/
- public boolean beginBatchEdit();
+ boolean beginBatchEdit();
/**
* Tell the editor that you are done with a batch edit previously
@@ -696,7 +696,7 @@
* the latest one (in other words, if the nesting count is > 0), false
* otherwise or if the input connection is no longer valid.
*/
- public boolean endBatchEdit();
+ boolean endBatchEdit();
/**
* Send a key event to the process that is currently attached
@@ -734,7 +734,7 @@
* @see KeyCharacterMap#PREDICTIVE
* @see KeyCharacterMap#ALPHA
*/
- public boolean sendKeyEvent(KeyEvent event);
+ boolean sendKeyEvent(KeyEvent event);
/**
* Clear the given meta key pressed states in the given input
@@ -749,7 +749,7 @@
* @return true on success, false if the input connection is no longer
* valid.
*/
- public boolean clearMetaKeyStates(int states);
+ boolean clearMetaKeyStates(int states);
/**
* Called back when the connected IME switches between fullscreen and normal modes.
@@ -766,7 +766,7 @@
* devices.
* @see InputMethodManager#isFullscreenMode()
*/
- public boolean reportFullscreenMode(boolean enabled);
+ boolean reportFullscreenMode(boolean enabled);
/**
* API to send private commands from an input method to its
@@ -786,7 +786,7 @@
* associated editor understood it), false if the input connection is no longer
* valid.
*/
- public boolean performPrivateCommand(String action, Bundle data);
+ boolean performPrivateCommand(String action, Bundle data);
/**
* The editor is requested to call
@@ -794,7 +794,7 @@
* once, as soon as possible, regardless of cursor/anchor position changes. This flag can be
* used together with {@link #CURSOR_UPDATE_MONITOR}.
*/
- public static final int CURSOR_UPDATE_IMMEDIATE = 1 << 0;
+ int CURSOR_UPDATE_IMMEDIATE = 1 << 0;
/**
* The editor is requested to call
@@ -805,7 +805,7 @@
* This flag can be used together with {@link #CURSOR_UPDATE_IMMEDIATE}.
* </p>
*/
- public static final int CURSOR_UPDATE_MONITOR = 1 << 1;
+ int CURSOR_UPDATE_MONITOR = 1 << 1;
/**
* Called by the input method to ask the editor for calling back
@@ -821,7 +821,7 @@
* In {@link android.os.Build.VERSION_CODES#N} and later, returns {@code false} also when the
* target application does not implement this method.
*/
- public boolean requestCursorUpdates(int cursorUpdateMode);
+ boolean requestCursorUpdates(int cursorUpdateMode);
/**
* Called by the {@link InputMethodManager} to enable application developers to specify a
@@ -832,7 +832,7 @@
*
* @return {@code null} to use the default {@link Handler}.
*/
- public Handler getHandler();
+ Handler getHandler();
/**
* Called by the system up to only once to notify that the system is about to invalidate
@@ -846,7 +846,7 @@
*
* <p>Note: This does nothing when called from input methods.</p>
*/
- public void closeConnection();
+ void closeConnection();
/**
* When this flag is used, the editor will be able to request read access to the content URI
@@ -863,7 +863,7 @@
* client is able to request a temporary read-only access even after the current IME is switched
* to any other IME as long as the client keeps {@link InputContentInfo} object.</p>
**/
- public static int INPUT_CONTENT_GRANT_READ_URI_PERMISSION =
+ int INPUT_CONTENT_GRANT_READ_URI_PERMISSION =
android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; // 0x00000001
/**
@@ -897,6 +897,39 @@
* @return {@code true} if this request is accepted by the application, whether the request
* is already handled or still being handled in background, {@code false} otherwise.
*/
- public boolean commitContent(@NonNull InputContentInfo inputContentInfo, int flags,
+ boolean commitContent(@NonNull InputContentInfo inputContentInfo, int flags,
@Nullable Bundle opts);
+
+ /**
+ * Called by the input method to tell a hint about the locales of text to be committed.
+ *
+ * <p>This is just a hint for editor authors (and the system) to choose better options when
+ * they have to disambiguate languages, like editor authors can do for input methods with
+ * {@link EditorInfo#hintLocales}.</p>
+ *
+ * <p>The language hint provided by this callback should have higher priority than
+ * {@link InputMethodSubtype#getLanguageTag()}, which cannot be updated dynamically.</p>
+ *
+ * <p>Note that in general it is discouraged for input method to specify
+ * {@link android.text.style.LocaleSpan} when inputting text, mainly because of application
+ * compatibility concerns.</p>
+ * <ul>
+ * <li>When an existing text that already has {@link android.text.style.LocaleSpan} is being
+ * modified by both the input method and application, there is no reliable and easy way to
+ * keep track of who modified {@link android.text.style.LocaleSpan}. For instance, if the
+ * text was updated by JavaScript, it it highly likely that span information is completely
+ * removed, while some input method attempts to preserve spans if possible.</li>
+ * <li>There is no clear semantics regarding whether {@link android.text.style.LocaleSpan}
+ * means a weak (ignorable) hint or a strong hint. This becomes more problematic when
+ * multiple {@link android.text.style.LocaleSpan} instances are specified to the same
+ * text region, especially when those spans are conflicting.</li>
+ * </ul>
+ * @param languageHint list of languages sorted by the priority and/or probability
+ */
+ default void reportLanguageHint(@NonNull LocaleList languageHint) {
+ // Intentionally empty.
+ //
+ // We need to have *some* default implementation for the source compatibility.
+ // See Bug 72127682 for details.
+ }
}
diff --git a/android/view/inputmethod/InputConnectionWrapper.java b/android/view/inputmethod/InputConnectionWrapper.java
index 317730c..cbe6856 100644
--- a/android/view/inputmethod/InputConnectionWrapper.java
+++ b/android/view/inputmethod/InputConnectionWrapper.java
@@ -1,23 +1,25 @@
/*
- * Copyright (C) 2007-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
- *
+ * Copyright (C) 2007 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.
+ * 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.inputmethod;
+import android.annotation.NonNull;
import android.os.Bundle;
import android.os.Handler;
+import android.os.LocaleList;
import android.view.KeyEvent;
/**
@@ -74,6 +76,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public CharSequence getTextBeforeCursor(int n, int flags) {
return mTarget.getTextBeforeCursor(n, flags);
}
@@ -82,6 +85,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public CharSequence getTextAfterCursor(int n, int flags) {
return mTarget.getTextAfterCursor(n, flags);
}
@@ -90,6 +94,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public CharSequence getSelectedText(int flags) {
return mTarget.getSelectedText(flags);
}
@@ -98,6 +103,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public int getCursorCapsMode(int reqModes) {
return mTarget.getCursorCapsMode(reqModes);
}
@@ -106,6 +112,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
return mTarget.getExtractedText(request, flags);
}
@@ -114,6 +121,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
return mTarget.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
}
@@ -122,6 +130,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
return mTarget.deleteSurroundingText(beforeLength, afterLength);
}
@@ -130,6 +139,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
return mTarget.setComposingText(text, newCursorPosition);
}
@@ -138,6 +148,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean setComposingRegion(int start, int end) {
return mTarget.setComposingRegion(start, end);
}
@@ -146,6 +157,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean finishComposingText() {
return mTarget.finishComposingText();
}
@@ -154,6 +166,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean commitText(CharSequence text, int newCursorPosition) {
return mTarget.commitText(text, newCursorPosition);
}
@@ -162,6 +175,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean commitCompletion(CompletionInfo text) {
return mTarget.commitCompletion(text);
}
@@ -170,6 +184,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean commitCorrection(CorrectionInfo correctionInfo) {
return mTarget.commitCorrection(correctionInfo);
}
@@ -178,6 +193,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean setSelection(int start, int end) {
return mTarget.setSelection(start, end);
}
@@ -186,6 +202,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean performEditorAction(int editorAction) {
return mTarget.performEditorAction(editorAction);
}
@@ -194,6 +211,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean performContextMenuAction(int id) {
return mTarget.performContextMenuAction(id);
}
@@ -202,6 +220,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean beginBatchEdit() {
return mTarget.beginBatchEdit();
}
@@ -210,6 +229,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean endBatchEdit() {
return mTarget.endBatchEdit();
}
@@ -218,6 +238,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean sendKeyEvent(KeyEvent event) {
return mTarget.sendKeyEvent(event);
}
@@ -226,6 +247,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean clearMetaKeyStates(int states) {
return mTarget.clearMetaKeyStates(states);
}
@@ -234,6 +256,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean reportFullscreenMode(boolean enabled) {
return mTarget.reportFullscreenMode(enabled);
}
@@ -242,6 +265,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean performPrivateCommand(String action, Bundle data) {
return mTarget.performPrivateCommand(action, data);
}
@@ -250,6 +274,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean requestCursorUpdates(int cursorUpdateMode) {
return mTarget.requestCursorUpdates(cursorUpdateMode);
}
@@ -258,6 +283,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public Handler getHandler() {
return mTarget.getHandler();
}
@@ -266,6 +292,7 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public void closeConnection() {
mTarget.closeConnection();
}
@@ -274,7 +301,17 @@
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
return mTarget.commitContent(inputContentInfo, flags, opts);
}
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public void reportLanguageHint(@NonNull LocaleList languageHint) {
+ mTarget.reportLanguageHint(languageHint);
+ }
}
diff --git a/android/view/inputmethod/InputMethodManager.java b/android/view/inputmethod/InputMethodManager.java
index 80d7b6b..7db5c32 100644
--- a/android/view/inputmethod/InputMethodManager.java
+++ b/android/view/inputmethod/InputMethodManager.java
@@ -337,20 +337,23 @@
int mCursorCandEnd;
/**
- * Represents an invalid action notification sequence number. {@link InputMethodManagerService}
- * always issues a positive integer for action notification sequence numbers. Thus -1 is
- * guaranteed to be different from any valid sequence number.
+ * Represents an invalid action notification sequence number.
+ * {@link com.android.server.InputMethodManagerService} always issues a positive integer for
+ * action notification sequence numbers. Thus {@code -1} is guaranteed to be different from any
+ * valid sequence number.
*/
private static final int NOT_AN_ACTION_NOTIFICATION_SEQUENCE_NUMBER = -1;
/**
- * The next sequence number that is to be sent to {@link InputMethodManagerService} via
+ * The next sequence number that is to be sent to
+ * {@link com.android.server.InputMethodManagerService} via
* {@link IInputMethodManager#notifyUserAction(int)} at once when a user action is observed.
*/
private int mNextUserActionNotificationSequenceNumber =
NOT_AN_ACTION_NOTIFICATION_SEQUENCE_NUMBER;
/**
- * The last sequence number that is already sent to {@link InputMethodManagerService}.
+ * The last sequence number that is already sent to
+ * {@link com.android.server.InputMethodManagerService}.
*/
private int mLastSentUserActionNotificationSequenceNumber =
NOT_AN_ACTION_NOTIFICATION_SEQUENCE_NUMBER;
@@ -1079,15 +1082,15 @@
}
/**
- * Flag for {@link #hideSoftInputFromWindow} to indicate that the soft
- * input window should only be hidden if it was not explicitly shown
+ * Flag for {@link #hideSoftInputFromWindow} and {@link InputMethodService#requestHideSelf(int)}
+ * to indicate that the soft input window should only be hidden if it was not explicitly shown
* by the user.
*/
public static final int HIDE_IMPLICIT_ONLY = 0x0001;
/**
- * Flag for {@link #hideSoftInputFromWindow} to indicate that the soft
- * input window should normally be hidden, unless it was originally
+ * Flag for {@link #hideSoftInputFromWindow} and {@link InputMethodService#requestShowSelf(int)}
+ * to indicate that the soft input window should normally be hidden, unless it was originally
* shown with {@link #SHOW_FORCED}.
*/
public static final int HIDE_NOT_ALWAYS = 0x0002;
@@ -1255,12 +1258,7 @@
// The view is running on a different thread than our own, so
// we need to reschedule our work for over there.
if (DEBUG) Log.v(TAG, "Starting input: reschedule to view thread");
- vh.post(new Runnable() {
- @Override
- public void run() {
- startInputInner(startInputReason, null, 0, 0, 0);
- }
- });
+ vh.post(() -> startInputInner(startInputReason, null, 0, 0, 0));
return false;
}
@@ -1871,9 +1869,9 @@
* @param flags Provides additional operating flags. Currently may be
* 0 or have the {@link #HIDE_IMPLICIT_ONLY},
* {@link #HIDE_NOT_ALWAYS} bit set.
- * @deprecated Use {@link InputMethodService#hideSoftInputFromInputMethod(int)}
- * instead. This method was intended for IME developers who should be accessing APIs through
- * the service. APIs in this class are intended for app developers interacting with the IME.
+ * @deprecated Use {@link InputMethodService#requestHideSelf(int)} instead. This method was
+ * intended for IME developers who should be accessing APIs through the service. APIs in this
+ * class are intended for app developers interacting with the IME.
*/
@Deprecated
public void hideSoftInputFromInputMethod(IBinder token, int flags) {
@@ -1903,9 +1901,9 @@
* @param flags Provides additional operating flags. Currently may be
* 0 or have the {@link #SHOW_IMPLICIT} or
* {@link #SHOW_FORCED} bit set.
- * @deprecated Use {@link InputMethodService#showSoftInputFromInputMethod(int)}
- * instead. This method was intended for IME developers who should be accessing APIs through
- * the service. APIs in this class are intended for app developers interacting with the IME.
+ * @deprecated Use {@link InputMethodService#requestShowSelf(int)} instead. This method was
+ * intended for IME developers who should be accessing APIs through the service. APIs in this
+ * class are intended for app developers interacting with the IME.
*/
@Deprecated
public void showSoftInputFromInputMethod(IBinder token, int flags) {
@@ -2429,8 +2427,8 @@
* Allow the receiver of {@link InputContentInfo} to obtain a temporary read-only access
* permission to the content.
*
- * <p>See {@link android.inputmethodservice.InputMethodService#exposeContent(InputContentInfo, EditorInfo)}
- * for details.</p>
+ * <p>See {@link android.inputmethodservice.InputMethodService#exposeContent(InputContentInfo,
+ * InputConnection)} for details.</p>
*
* @param token Supplies the identifying token given to an input method when it was started,
* which allows it to perform this operation on itself.
diff --git a/android/view/textclassifier/EntityConfidence.java b/android/view/textclassifier/EntityConfidence.java
index 19660d9..69a59a5 100644
--- a/android/view/textclassifier/EntityConfidence.java
+++ b/android/view/textclassifier/EntityConfidence.java
@@ -18,6 +18,8 @@
import android.annotation.FloatRange;
import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.util.ArrayMap;
import com.android.internal.util.Preconditions;
@@ -30,17 +32,16 @@
/**
* Helper object for setting and getting entity scores for classified text.
*
- * @param <T> the entity type.
* @hide
*/
-final class EntityConfidence<T> {
+final class EntityConfidence implements Parcelable {
- private final ArrayMap<T, Float> mEntityConfidence = new ArrayMap<>();
- private final ArrayList<T> mSortedEntities = new ArrayList<>();
+ private final ArrayMap<String, Float> mEntityConfidence = new ArrayMap<>();
+ private final ArrayList<String> mSortedEntities = new ArrayList<>();
EntityConfidence() {}
- EntityConfidence(@NonNull EntityConfidence<T> source) {
+ EntityConfidence(@NonNull EntityConfidence source) {
Preconditions.checkNotNull(source);
mEntityConfidence.putAll(source.mEntityConfidence);
mSortedEntities.addAll(source.mSortedEntities);
@@ -54,24 +55,16 @@
* @param source a map from entity to a confidence value in the range 0 (low confidence) to
* 1 (high confidence).
*/
- EntityConfidence(@NonNull Map<T, Float> source) {
+ EntityConfidence(@NonNull Map<String, Float> source) {
Preconditions.checkNotNull(source);
// Prune non-existent entities and clamp to 1.
mEntityConfidence.ensureCapacity(source.size());
- for (Map.Entry<T, Float> it : source.entrySet()) {
+ for (Map.Entry<String, Float> it : source.entrySet()) {
if (it.getValue() <= 0) continue;
mEntityConfidence.put(it.getKey(), Math.min(1, it.getValue()));
}
-
- // Create a list of entities sorted by decreasing confidence for getEntities().
- mSortedEntities.ensureCapacity(mEntityConfidence.size());
- mSortedEntities.addAll(mEntityConfidence.keySet());
- mSortedEntities.sort((e1, e2) -> {
- float score1 = mEntityConfidence.get(e1);
- float score2 = mEntityConfidence.get(e2);
- return Float.compare(score2, score1);
- });
+ resetSortedEntitiesFromMap();
}
/**
@@ -79,7 +72,7 @@
* high confidence to low confidence.
*/
@NonNull
- public List<T> getEntities() {
+ public List<String> getEntities() {
return Collections.unmodifiableList(mSortedEntities);
}
@@ -89,7 +82,7 @@
* classified text.
*/
@FloatRange(from = 0.0, to = 1.0)
- public float getConfidenceScore(T entity) {
+ public float getConfidenceScore(String entity) {
if (mEntityConfidence.containsKey(entity)) {
return mEntityConfidence.get(entity);
}
@@ -100,4 +93,51 @@
public String toString() {
return mEntityConfidence.toString();
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mEntityConfidence.size());
+ for (Map.Entry<String, Float> entry : mEntityConfidence.entrySet()) {
+ dest.writeString(entry.getKey());
+ dest.writeFloat(entry.getValue());
+ }
+ }
+
+ public static final Parcelable.Creator<EntityConfidence> CREATOR =
+ new Parcelable.Creator<EntityConfidence>() {
+ @Override
+ public EntityConfidence createFromParcel(Parcel in) {
+ return new EntityConfidence(in);
+ }
+
+ @Override
+ public EntityConfidence[] newArray(int size) {
+ return new EntityConfidence[size];
+ }
+ };
+
+ private EntityConfidence(Parcel in) {
+ final int numEntities = in.readInt();
+ mEntityConfidence.ensureCapacity(numEntities);
+ for (int i = 0; i < numEntities; ++i) {
+ mEntityConfidence.put(in.readString(), in.readFloat());
+ }
+ resetSortedEntitiesFromMap();
+ }
+
+ private void resetSortedEntitiesFromMap() {
+ mSortedEntities.clear();
+ mSortedEntities.ensureCapacity(mEntityConfidence.size());
+ mSortedEntities.addAll(mEntityConfidence.keySet());
+ mSortedEntities.sort((e1, e2) -> {
+ float score1 = mEntityConfidence.get(e1);
+ float score2 = mEntityConfidence.get(e2);
+ return Float.compare(score2, score1);
+ });
+ }
}
diff --git a/android/view/textclassifier/TextClassification.java b/android/view/textclassifier/TextClassification.java
index 7ffbf63..7089677 100644
--- a/android/view/textclassifier/TextClassification.java
+++ b/android/view/textclassifier/TextClassification.java
@@ -22,8 +22,13 @@
import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.util.ArrayMap;
import android.view.View.OnClickListener;
import android.view.textclassifier.TextClassifier.EntityType;
@@ -52,7 +57,7 @@
* Button button = new Button(context);
* button.setCompoundDrawablesWithIntrinsicBounds(classification.getIcon(), null, null, null);
* button.setText(classification.getLabel());
- * button.setOnClickListener(classification.getOnClickListener());
+ * button.setOnClickListener(v -> context.startActivity(classification.getIntent()));
* }</pre>
*
* <p>e.g. starting an action mode with menu items that can handle the classified text:
@@ -90,7 +95,6 @@
* ...
* });
* }</pre>
- *
*/
public final class TextClassification {
@@ -99,6 +103,10 @@
*/
static final TextClassification EMPTY = new TextClassification.Builder().build();
+ // TODO(toki): investigate a way to derive this based on device properties.
+ private static final int MAX_PRIMARY_ICON_SIZE = 192;
+ private static final int MAX_SECONDARY_ICON_SIZE = 144;
+
@NonNull private final String mText;
@Nullable private final Drawable mPrimaryIcon;
@Nullable private final String mPrimaryLabel;
@@ -107,8 +115,7 @@
@NonNull private final List<Drawable> mSecondaryIcons;
@NonNull private final List<String> mSecondaryLabels;
@NonNull private final List<Intent> mSecondaryIntents;
- @NonNull private final List<OnClickListener> mSecondaryOnClickListeners;
- @NonNull private final EntityConfidence<String> mEntityConfidence;
+ @NonNull private final EntityConfidence mEntityConfidence;
@NonNull private final String mSignature;
private TextClassification(
@@ -120,12 +127,10 @@
@NonNull List<Drawable> secondaryIcons,
@NonNull List<String> secondaryLabels,
@NonNull List<Intent> secondaryIntents,
- @NonNull List<OnClickListener> secondaryOnClickListeners,
@NonNull Map<String, Float> entityConfidence,
@NonNull String signature) {
Preconditions.checkArgument(secondaryLabels.size() == secondaryIntents.size());
Preconditions.checkArgument(secondaryIcons.size() == secondaryIntents.size());
- Preconditions.checkArgument(secondaryOnClickListeners.size() == secondaryIntents.size());
mText = text;
mPrimaryIcon = primaryIcon;
mPrimaryLabel = primaryLabel;
@@ -134,8 +139,7 @@
mSecondaryIcons = secondaryIcons;
mSecondaryLabels = secondaryLabels;
mSecondaryIntents = secondaryIntents;
- mSecondaryOnClickListeners = secondaryOnClickListeners;
- mEntityConfidence = new EntityConfidence<>(entityConfidence);
+ mEntityConfidence = new EntityConfidence(entityConfidence);
mSignature = signature;
}
@@ -186,7 +190,6 @@
* @see #getSecondaryIntent(int)
* @see #getSecondaryLabel(int)
* @see #getSecondaryIcon(int)
- * @see #getSecondaryOnClickListener(int)
*/
@IntRange(from = 0)
public int getSecondaryActionsCount() {
@@ -198,13 +201,10 @@
* classified text.
*
* @param index Index of the action to get the icon for.
- *
* @throws IndexOutOfBoundsException if the specified index is out of range.
- *
* @see #getSecondaryActionsCount() for the number of actions available.
* @see #getSecondaryIntent(int)
* @see #getSecondaryLabel(int)
- * @see #getSecondaryOnClickListener(int)
* @see #getIcon()
*/
@Nullable
@@ -228,13 +228,10 @@
* the classified text.
*
* @param index Index of the action to get the label for.
- *
* @throws IndexOutOfBoundsException if the specified index is out of range.
- *
* @see #getSecondaryActionsCount()
* @see #getSecondaryIntent(int)
* @see #getSecondaryIcon(int)
- * @see #getSecondaryOnClickListener(int)
* @see #getLabel()
*/
@Nullable
@@ -257,13 +254,10 @@
* Returns one of the <i>secondary</i> intents that may be fired to act on the classified text.
*
* @param index Index of the action to get the intent for.
- *
* @throws IndexOutOfBoundsException if the specified index is out of range.
- *
* @see #getSecondaryActionsCount()
* @see #getSecondaryLabel(int)
* @see #getSecondaryIcon(int)
- * @see #getSecondaryOnClickListener(int)
* @see #getIntent()
*/
@Nullable
@@ -282,29 +276,10 @@
}
/**
- * Returns one of the <i>secondary</i> OnClickListeners that may be triggered to act on the
- * classified text.
- *
- * @param index Index of the action to get the click listener for.
- *
- * @throws IndexOutOfBoundsException if the specified index is out of range.
- *
- * @see #getSecondaryActionsCount()
- * @see #getSecondaryIntent(int)
- * @see #getSecondaryLabel(int)
- * @see #getSecondaryIcon(int)
- * @see #getOnClickListener()
- */
- @Nullable
- public OnClickListener getSecondaryOnClickListener(int index) {
- return mSecondaryOnClickListeners.get(index);
- }
-
- /**
* Returns the <i>primary</i> OnClickListener that may be triggered to act on the classified
- * text.
- *
- * @see #getSecondaryOnClickListener(int)
+ * text. This field is not parcelable and will be null for all objects read from a parcel.
+ * Instead, call Context#startActivity(Intent) with the result of #getSecondaryIntent(int).
+ * Note that this may fail if the activity doesn't have permission to send the intent.
*/
@Nullable
public OnClickListener getOnClickListener() {
@@ -334,6 +309,42 @@
mSignature);
}
+ /** Helper for parceling via #ParcelableWrapper. */
+ private void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mText);
+ final Bitmap primaryIconBitmap = drawableToBitmap(mPrimaryIcon, MAX_PRIMARY_ICON_SIZE);
+ dest.writeInt(primaryIconBitmap != null ? 1 : 0);
+ if (primaryIconBitmap != null) {
+ primaryIconBitmap.writeToParcel(dest, flags);
+ }
+ dest.writeString(mPrimaryLabel);
+ dest.writeInt(mPrimaryIntent != null ? 1 : 0);
+ if (mPrimaryIntent != null) {
+ mPrimaryIntent.writeToParcel(dest, flags);
+ }
+ // mPrimaryOnClickListener is not parcelable.
+ dest.writeTypedList(drawablesToBitmaps(mSecondaryIcons, MAX_SECONDARY_ICON_SIZE));
+ dest.writeStringList(mSecondaryLabels);
+ dest.writeTypedList(mSecondaryIntents);
+ mEntityConfidence.writeToParcel(dest, flags);
+ dest.writeString(mSignature);
+ }
+
+ /** Helper for unparceling via #ParcelableWrapper. */
+ private TextClassification(Parcel in) {
+ mText = in.readString();
+ mPrimaryIcon = in.readInt() == 0
+ ? null : new BitmapDrawable(null, Bitmap.CREATOR.createFromParcel(in));
+ mPrimaryLabel = in.readString();
+ mPrimaryIntent = in.readInt() == 0 ? null : Intent.CREATOR.createFromParcel(in);
+ mPrimaryOnClickListener = null; // not parcelable
+ mSecondaryIcons = bitmapsToDrawables(in.createTypedArrayList(Bitmap.CREATOR));
+ mSecondaryLabels = in.createStringArrayList();
+ mSecondaryIntents = in.createTypedArrayList(Intent.CREATOR);
+ mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
+ mSignature = in.readString();
+ }
+
/**
* Creates an OnClickListener that starts an activity with the specified intent.
*
@@ -349,6 +360,68 @@
}
/**
+ * Returns a Bitmap representation of the Drawable
+ *
+ * @param drawable The drawable to convert.
+ * @param maxDims The maximum edge length of the resulting bitmap (in pixels).
+ */
+ @Nullable
+ private static Bitmap drawableToBitmap(@Nullable Drawable drawable, int maxDims) {
+ if (drawable == null) {
+ return null;
+ }
+ final int actualWidth = Math.max(1, drawable.getIntrinsicWidth());
+ final int actualHeight = Math.max(1, drawable.getIntrinsicHeight());
+ final double scaleWidth = ((double) maxDims) / actualWidth;
+ final double scaleHeight = ((double) maxDims) / actualHeight;
+ final double scale = Math.min(1.0, Math.min(scaleWidth, scaleHeight));
+ final int width = (int) (actualWidth * scale);
+ final int height = (int) (actualHeight * scale);
+ if (drawable instanceof BitmapDrawable) {
+ final BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+ if (actualWidth != width || actualHeight != height) {
+ return Bitmap.createScaledBitmap(
+ bitmapDrawable.getBitmap(), width, height, /*filter=*/false);
+ } else {
+ return bitmapDrawable.getBitmap();
+ }
+ } else {
+ final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ return bitmap;
+ }
+ }
+
+ /**
+ * Returns a list of drawables converted to Bitmaps
+ *
+ * @param drawables The drawables to convert.
+ * @param maxDims The maximum edge length of the resulting bitmaps (in pixels).
+ */
+ private static List<Bitmap> drawablesToBitmaps(List<Drawable> drawables, int maxDims) {
+ final List<Bitmap> bitmaps = new ArrayList<>(drawables.size());
+ for (Drawable drawable : drawables) {
+ bitmaps.add(drawableToBitmap(drawable, maxDims));
+ }
+ return bitmaps;
+ }
+
+ /** Returns a list of drawable wrappers for a list of bitmaps. */
+ private static List<Drawable> bitmapsToDrawables(List<Bitmap> bitmaps) {
+ final List<Drawable> drawables = new ArrayList<>(bitmaps.size());
+ for (Bitmap bitmap : bitmaps) {
+ if (bitmap != null) {
+ drawables.add(new BitmapDrawable(null, bitmap));
+ } else {
+ drawables.add(null);
+ }
+ }
+ return drawables;
+ }
+
+ /**
* Builder for building {@link TextClassification} objects.
*
* <p>e.g.
@@ -358,9 +431,9 @@
* .setText(classifiedText)
* .setEntityType(TextClassifier.TYPE_EMAIL, 0.9)
* .setEntityType(TextClassifier.TYPE_OTHER, 0.1)
- * .setPrimaryAction(intent, label, icon, onClickListener)
- * .addSecondaryAction(intent1, label1, icon1, onClickListener1)
- * .addSecondaryAction(intent2, label2, icon2, onClickListener2)
+ * .setPrimaryAction(intent, label, icon)
+ * .addSecondaryAction(intent1, label1, icon1)
+ * .addSecondaryAction(intent2, label2, icon2)
* .build();
* }</pre>
*/
@@ -370,7 +443,6 @@
@NonNull private final List<Drawable> mSecondaryIcons = new ArrayList<>();
@NonNull private final List<String> mSecondaryLabels = new ArrayList<>();
@NonNull private final List<Intent> mSecondaryIntents = new ArrayList<>();
- @NonNull private final List<OnClickListener> mSecondaryOnClickListeners = new ArrayList<>();
@NonNull private final Map<String, Float> mEntityConfidence = new ArrayMap<>();
@Nullable Drawable mPrimaryIcon;
@Nullable String mPrimaryLabel;
@@ -413,16 +485,14 @@
* <p><stong>Note: </stong> If all input parameters are set to null, this method will be a
* no-op.
*
- * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener)
+ * @see #setPrimaryAction(Intent, String, Drawable)
*/
public Builder addSecondaryAction(
- @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon,
- @Nullable OnClickListener onClickListener) {
- if (intent != null || label != null || icon != null || onClickListener != null) {
+ @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon) {
+ if (intent != null || label != null || icon != null) {
mSecondaryIntents.add(intent);
mSecondaryLabels.add(label);
mSecondaryIcons.add(icon);
- mSecondaryOnClickListeners.add(onClickListener);
}
return this;
}
@@ -432,7 +502,6 @@
*/
public Builder clearSecondaryActions() {
mSecondaryIntents.clear();
- mSecondaryOnClickListeners.clear();
mSecondaryLabels.clear();
mSecondaryIcons.clear();
return this;
@@ -440,26 +509,23 @@
/**
* Sets the <i>primary</i> action that may be performed on the classified text. This is
- * equivalent to calling {@code
- * setIntent(intent).setLabel(label).setIcon(icon).setOnClickListener(onClickListener)}.
+ * equivalent to calling {@code setIntent(intent).setLabel(label).setIcon(icon)}.
*
* <p><strong>Note: </strong>If all input parameters are null, there will be no
* <i>primary</i> action but there may still be <i>secondary</i> actions.
*
- * @see #addSecondaryAction(Intent, String, Drawable, OnClickListener)
+ * @see #addSecondaryAction(Intent, String, Drawable)
*/
public Builder setPrimaryAction(
- @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon,
- @Nullable OnClickListener onClickListener) {
- return setIntent(intent).setLabel(label).setIcon(icon)
- .setOnClickListener(onClickListener);
+ @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon) {
+ return setIntent(intent).setLabel(label).setIcon(icon);
}
/**
* Sets the icon for the <i>primary</i> action that may be rendered on a widget used to act
* on the classified text.
*
- * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener)
+ * @see #setPrimaryAction(Intent, String, Drawable)
*/
public Builder setIcon(@Nullable Drawable icon) {
mPrimaryIcon = icon;
@@ -470,7 +536,7 @@
* Sets the label for the <i>primary</i> action that may be rendered on a widget used to
* act on the classified text.
*
- * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener)
+ * @see #setPrimaryAction(Intent, String, Drawable)
*/
public Builder setLabel(@Nullable String label) {
mPrimaryLabel = label;
@@ -481,7 +547,7 @@
* Sets the intent for the <i>primary</i> action that may be fired to act on the classified
* text.
*
- * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener)
+ * @see #setPrimaryAction(Intent, String, Drawable)
*/
public Builder setIntent(@Nullable Intent intent) {
mPrimaryIntent = intent;
@@ -490,9 +556,8 @@
/**
* Sets the OnClickListener for the <i>primary</i> action that may be triggered to act on
- * the classified text.
- *
- * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener)
+ * the classified text. This field is not parcelable and will always be null when the
+ * object is read from a parcel.
*/
public Builder setOnClickListener(@Nullable OnClickListener onClickListener) {
mPrimaryOnClickListener = onClickListener;
@@ -515,10 +580,8 @@
public TextClassification build() {
return new TextClassification(
mText,
- mPrimaryIcon, mPrimaryLabel,
- mPrimaryIntent, mPrimaryOnClickListener,
- mSecondaryIcons, mSecondaryLabels,
- mSecondaryIntents, mSecondaryOnClickListeners,
+ mPrimaryIcon, mPrimaryLabel, mPrimaryIntent, mPrimaryOnClickListener,
+ mSecondaryIcons, mSecondaryLabels, mSecondaryIntents,
mEntityConfidence, mSignature);
}
}
@@ -526,9 +589,11 @@
/**
* Optional input parameters for generating TextClassification.
*/
- public static final class Options {
+ public static final class Options implements Parcelable {
- private LocaleList mDefaultLocales;
+ private @Nullable LocaleList mDefaultLocales;
+
+ public Options() {}
/**
* @param defaultLocales ordered list of locale preferences that may be used to disambiguate
@@ -548,5 +613,80 @@
public LocaleList getDefaultLocales() {
return mDefaultLocales;
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mDefaultLocales != null ? 1 : 0);
+ if (mDefaultLocales != null) {
+ mDefaultLocales.writeToParcel(dest, flags);
+ }
+ }
+
+ public static final Parcelable.Creator<Options> CREATOR =
+ new Parcelable.Creator<Options>() {
+ @Override
+ public Options createFromParcel(Parcel in) {
+ return new Options(in);
+ }
+
+ @Override
+ public Options[] newArray(int size) {
+ return new Options[size];
+ }
+ };
+
+ private Options(Parcel in) {
+ if (in.readInt() > 0) {
+ mDefaultLocales = LocaleList.CREATOR.createFromParcel(in);
+ }
+ }
+ }
+
+ /**
+ * Parcelable wrapper for TextClassification objects.
+ * @hide
+ */
+ public static final class ParcelableWrapper implements Parcelable {
+
+ @NonNull private TextClassification mTextClassification;
+
+ public ParcelableWrapper(@NonNull TextClassification textClassification) {
+ Preconditions.checkNotNull(textClassification);
+ mTextClassification = textClassification;
+ }
+
+ @NonNull
+ public TextClassification getTextClassification() {
+ return mTextClassification;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ mTextClassification.writeToParcel(dest, flags);
+ }
+
+ public static final Parcelable.Creator<ParcelableWrapper> CREATOR =
+ new Parcelable.Creator<ParcelableWrapper>() {
+ @Override
+ public ParcelableWrapper createFromParcel(Parcel in) {
+ return new ParcelableWrapper(new TextClassification(in));
+ }
+
+ @Override
+ public ParcelableWrapper[] newArray(int size) {
+ return new ParcelableWrapper[size];
+ }
+ };
+
}
}
diff --git a/android/view/textclassifier/TextClassifier.java b/android/view/textclassifier/TextClassifier.java
index ed60430..e9715c5 100644
--- a/android/view/textclassifier/TextClassifier.java
+++ b/android/view/textclassifier/TextClassifier.java
@@ -23,6 +23,8 @@
import android.annotation.StringDef;
import android.annotation.WorkerThread;
import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.util.ArraySet;
import com.android.internal.util.Preconditions;
@@ -275,8 +277,8 @@
/**
* Returns a {@link Collection} of the entity types in the specified preset.
*
- * @see #ENTITIES_ALL
- * @see #ENTITIES_NONE
+ * @see #ENTITY_PRESET_ALL
+ * @see #ENTITY_PRESET_NONE
*/
default Collection<String> getEntitiesForPreset(@EntityPreset int entityPreset) {
return Collections.EMPTY_LIST;
@@ -305,7 +307,7 @@
*
* Configs are initially based on a predefined preset, and can be modified from there.
*/
- final class EntityConfig {
+ final class EntityConfig implements Parcelable {
private final @TextClassifier.EntityPreset int mEntityPreset;
private final Collection<String> mExcludedEntityTypes;
private final Collection<String> mIncludedEntityTypes;
@@ -355,6 +357,37 @@
}
return Collections.unmodifiableList(entities);
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mEntityPreset);
+ dest.writeStringList(new ArrayList<>(mExcludedEntityTypes));
+ dest.writeStringList(new ArrayList<>(mIncludedEntityTypes));
+ }
+
+ public static final Parcelable.Creator<EntityConfig> CREATOR =
+ new Parcelable.Creator<EntityConfig>() {
+ @Override
+ public EntityConfig createFromParcel(Parcel in) {
+ return new EntityConfig(in);
+ }
+
+ @Override
+ public EntityConfig[] newArray(int size) {
+ return new EntityConfig[size];
+ }
+ };
+
+ private EntityConfig(Parcel in) {
+ mEntityPreset = in.readInt();
+ mExcludedEntityTypes = new ArraySet<>(in.createStringArrayList());
+ mIncludedEntityTypes = new ArraySet<>(in.createStringArrayList());
+ }
}
/**
diff --git a/android/view/textclassifier/TextClassifierConstants.java b/android/view/textclassifier/TextClassifierConstants.java
index 51e6168..00695b7 100644
--- a/android/view/textclassifier/TextClassifierConstants.java
+++ b/android/view/textclassifier/TextClassifierConstants.java
@@ -45,19 +45,24 @@
"smart_selection_dark_launch";
private static final String SMART_SELECTION_ENABLED_FOR_EDIT_TEXT =
"smart_selection_enabled_for_edit_text";
+ private static final String SMART_LINKIFY_ENABLED =
+ "smart_linkify_enabled";
private static final boolean SMART_SELECTION_DARK_LAUNCH_DEFAULT = false;
private static final boolean SMART_SELECTION_ENABLED_FOR_EDIT_TEXT_DEFAULT = true;
+ private static final boolean SMART_LINKIFY_ENABLED_DEFAULT = true;
/** Default settings. */
static final TextClassifierConstants DEFAULT = new TextClassifierConstants();
private final boolean mDarkLaunch;
private final boolean mSuggestSelectionEnabledForEditableText;
+ private final boolean mSmartLinkifyEnabled;
private TextClassifierConstants() {
mDarkLaunch = SMART_SELECTION_DARK_LAUNCH_DEFAULT;
mSuggestSelectionEnabledForEditableText = SMART_SELECTION_ENABLED_FOR_EDIT_TEXT_DEFAULT;
+ mSmartLinkifyEnabled = SMART_LINKIFY_ENABLED_DEFAULT;
}
private TextClassifierConstants(@Nullable String settings) {
@@ -74,6 +79,9 @@
mSuggestSelectionEnabledForEditableText = parser.getBoolean(
SMART_SELECTION_ENABLED_FOR_EDIT_TEXT,
SMART_SELECTION_ENABLED_FOR_EDIT_TEXT_DEFAULT);
+ mSmartLinkifyEnabled = parser.getBoolean(
+ SMART_LINKIFY_ENABLED,
+ SMART_LINKIFY_ENABLED_DEFAULT);
}
static TextClassifierConstants loadFromString(String settings) {
@@ -87,4 +95,8 @@
public boolean isSuggestSelectionEnabledForEditableText() {
return mSuggestSelectionEnabledForEditableText;
}
+
+ public boolean isSmartLinkifyEnabled() {
+ return mSmartLinkifyEnabled;
+ }
}
diff --git a/android/view/textclassifier/TextClassifierImpl.java b/android/view/textclassifier/TextClassifierImpl.java
index aea3cb0..7db0e76 100644
--- a/android/view/textclassifier/TextClassifierImpl.java
+++ b/android/view/textclassifier/TextClassifierImpl.java
@@ -32,7 +32,6 @@
import android.provider.Settings;
import android.text.util.Linkify;
import android.util.Patterns;
-import android.view.View.OnClickListener;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.logging.MetricsLogger;
@@ -187,6 +186,11 @@
Utils.validateInput(text);
final String textString = text.toString();
final TextLinks.Builder builder = new TextLinks.Builder(textString);
+
+ if (!getSettings().isSmartLinkifyEnabled()) {
+ return builder.build();
+ }
+
try {
final LocaleList defaultLocales = options != null ? options.getDefaultLocales() : null;
final Collection<String> entitiesToIdentify =
@@ -457,12 +461,10 @@
}
}
final String labelString = (label != null) ? label.toString() : null;
- final OnClickListener onClickListener =
- TextClassification.createStartActivityOnClickListener(mContext, intent);
if (i == 0) {
- builder.setPrimaryAction(intent, labelString, icon, onClickListener);
+ builder.setPrimaryAction(intent, labelString, icon);
} else {
- builder.addSecondaryAction(intent, labelString, icon, onClickListener);
+ builder.addSecondaryAction(intent, labelString, icon);
}
}
}
diff --git a/android/view/textclassifier/TextLinks.java b/android/view/textclassifier/TextLinks.java
index 6c587cf..ba854e0 100644
--- a/android/view/textclassifier/TextLinks.java
+++ b/android/view/textclassifier/TextLinks.java
@@ -20,6 +20,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.text.SpannableString;
import android.text.style.ClickableSpan;
import android.view.View;
@@ -38,7 +40,7 @@
* A collection of links, representing subsequences of text and the entity types (phone number,
* address, url, etc) they may be.
*/
-public final class TextLinks {
+public final class TextLinks implements Parcelable {
private final String mFullText;
private final List<TextLink> mLinks;
@@ -83,11 +85,40 @@
return true;
}
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mFullText);
+ dest.writeTypedList(mLinks);
+ }
+
+ public static final Parcelable.Creator<TextLinks> CREATOR =
+ new Parcelable.Creator<TextLinks>() {
+ @Override
+ public TextLinks createFromParcel(Parcel in) {
+ return new TextLinks(in);
+ }
+
+ @Override
+ public TextLinks[] newArray(int size) {
+ return new TextLinks[size];
+ }
+ };
+
+ private TextLinks(Parcel in) {
+ mFullText = in.readString();
+ mLinks = in.createTypedArrayList(TextLink.CREATOR);
+ }
+
/**
* A link, identifying a substring of text and possible entity types for it.
*/
- public static final class TextLink {
- private final EntityConfidence<String> mEntityScores;
+ public static final class TextLink implements Parcelable {
+ private final EntityConfidence mEntityScores;
private final String mOriginalText;
private final int mStart;
private final int mEnd;
@@ -105,7 +136,7 @@
mOriginalText = originalText;
mStart = start;
mEnd = end;
- mEntityScores = new EntityConfidence<>(entityScores);
+ mEntityScores = new EntityConfidence(entityScores);
}
/**
@@ -153,16 +184,51 @@
@TextClassifier.EntityType String entityType) {
return mEntityScores.getConfidenceScore(entityType);
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ mEntityScores.writeToParcel(dest, flags);
+ dest.writeString(mOriginalText);
+ dest.writeInt(mStart);
+ dest.writeInt(mEnd);
+ }
+
+ public static final Parcelable.Creator<TextLink> CREATOR =
+ new Parcelable.Creator<TextLink>() {
+ @Override
+ public TextLink createFromParcel(Parcel in) {
+ return new TextLink(in);
+ }
+
+ @Override
+ public TextLink[] newArray(int size) {
+ return new TextLink[size];
+ }
+ };
+
+ private TextLink(Parcel in) {
+ mEntityScores = EntityConfidence.CREATOR.createFromParcel(in);
+ mOriginalText = in.readString();
+ mStart = in.readInt();
+ mEnd = in.readInt();
+ }
}
/**
* Optional input parameters for generating TextLinks.
*/
- public static final class Options {
+ public static final class Options implements Parcelable {
private LocaleList mDefaultLocales;
private TextClassifier.EntityConfig mEntityConfig;
+ public Options() {}
+
/**
* @param defaultLocales ordered list of locale preferences that may be used to
* disambiguate the provided text. If no locale preferences exist,
@@ -201,6 +267,45 @@
public TextClassifier.EntityConfig getEntityConfig() {
return mEntityConfig;
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mDefaultLocales != null ? 1 : 0);
+ if (mDefaultLocales != null) {
+ mDefaultLocales.writeToParcel(dest, flags);
+ }
+ dest.writeInt(mEntityConfig != null ? 1 : 0);
+ if (mEntityConfig != null) {
+ mEntityConfig.writeToParcel(dest, flags);
+ }
+ }
+
+ public static final Parcelable.Creator<Options> CREATOR =
+ new Parcelable.Creator<Options>() {
+ @Override
+ public Options createFromParcel(Parcel in) {
+ return new Options(in);
+ }
+
+ @Override
+ public Options[] newArray(int size) {
+ return new Options[size];
+ }
+ };
+
+ private Options(Parcel in) {
+ if (in.readInt() > 0) {
+ mDefaultLocales = LocaleList.CREATOR.createFromParcel(in);
+ }
+ if (in.readInt() > 0) {
+ mEntityConfig = TextClassifier.EntityConfig.CREATOR.createFromParcel(in);
+ }
+ }
}
/**
diff --git a/android/view/textclassifier/TextSelection.java b/android/view/textclassifier/TextSelection.java
index 25e9e7e..774d42d 100644
--- a/android/view/textclassifier/TextSelection.java
+++ b/android/view/textclassifier/TextSelection.java
@@ -21,6 +21,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.util.ArrayMap;
import android.view.textclassifier.TextClassifier.EntityType;
@@ -36,7 +38,7 @@
private final int mStartIndex;
private final int mEndIndex;
- @NonNull private final EntityConfidence<String> mEntityConfidence;
+ @NonNull private final EntityConfidence mEntityConfidence;
@NonNull private final String mSignature;
private TextSelection(
@@ -44,7 +46,7 @@
@NonNull String signature) {
mStartIndex = startIndex;
mEndIndex = endIndex;
- mEntityConfidence = new EntityConfidence<>(entityConfidence);
+ mEntityConfidence = new EntityConfidence(entityConfidence);
mSignature = signature;
}
@@ -110,6 +112,22 @@
mStartIndex, mEndIndex, mEntityConfidence, mSignature);
}
+ /** Helper for parceling via #ParcelableWrapper. */
+ private void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mStartIndex);
+ dest.writeInt(mEndIndex);
+ mEntityConfidence.writeToParcel(dest, flags);
+ dest.writeString(mSignature);
+ }
+
+ /** Helper for unparceling via #ParcelableWrapper. */
+ private TextSelection(Parcel in) {
+ mStartIndex = in.readInt();
+ mEndIndex = in.readInt();
+ mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
+ mSignature = in.readString();
+ }
+
/**
* Builder used to build {@link TextSelection} objects.
*/
@@ -170,11 +188,13 @@
/**
* Optional input parameters for generating TextSelection.
*/
- public static final class Options {
+ public static final class Options implements Parcelable {
- private LocaleList mDefaultLocales;
+ private @Nullable LocaleList mDefaultLocales;
private boolean mDarkLaunchAllowed;
+ public Options() {}
+
/**
* @param defaultLocales ordered list of locale preferences that may be used to disambiguate
* the provided text. If no locale preferences exist, set this to null or an empty
@@ -216,5 +236,82 @@
public boolean isDarkLaunchAllowed() {
return mDarkLaunchAllowed;
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mDefaultLocales != null ? 1 : 0);
+ if (mDefaultLocales != null) {
+ mDefaultLocales.writeToParcel(dest, flags);
+ }
+ dest.writeInt(mDarkLaunchAllowed ? 1 : 0);
+ }
+
+ public static final Parcelable.Creator<Options> CREATOR =
+ new Parcelable.Creator<Options>() {
+ @Override
+ public Options createFromParcel(Parcel in) {
+ return new Options(in);
+ }
+
+ @Override
+ public Options[] newArray(int size) {
+ return new Options[size];
+ }
+ };
+
+ private Options(Parcel in) {
+ if (in.readInt() > 0) {
+ mDefaultLocales = LocaleList.CREATOR.createFromParcel(in);
+ }
+ mDarkLaunchAllowed = in.readInt() != 0;
+ }
+ }
+
+ /**
+ * Parcelable wrapper for TextSelection objects.
+ * @hide
+ */
+ public static final class ParcelableWrapper implements Parcelable {
+
+ @NonNull private TextSelection mTextSelection;
+
+ public ParcelableWrapper(@NonNull TextSelection textSelection) {
+ Preconditions.checkNotNull(textSelection);
+ mTextSelection = textSelection;
+ }
+
+ @NonNull
+ public TextSelection getTextSelection() {
+ return mTextSelection;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ mTextSelection.writeToParcel(dest, flags);
+ }
+
+ public static final Parcelable.Creator<ParcelableWrapper> CREATOR =
+ new Parcelable.Creator<ParcelableWrapper>() {
+ @Override
+ public ParcelableWrapper createFromParcel(Parcel in) {
+ return new ParcelableWrapper(new TextSelection(in));
+ }
+
+ @Override
+ public ParcelableWrapper[] newArray(int size) {
+ return new ParcelableWrapper[size];
+ }
+ };
+
}
}
diff --git a/android/webkit/FindAddress.java b/android/webkit/FindAddress.java
new file mode 100644
index 0000000..31b2427
--- /dev/null
+++ b/android/webkit/FindAddress.java
@@ -0,0 +1,478 @@
+/*
+ * Copyright (C) 2018 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.webkit;
+
+import java.util.Locale;
+import java.util.regex.MatchResult;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Java implementation of legacy WebView.findAddress algorithm.
+ *
+ * @hide
+ */
+class FindAddress {
+ static class ZipRange {
+ int mLow;
+ int mHigh;
+ int mException1;
+ int mException2;
+ ZipRange(int low, int high, int exception1, int exception2) {
+ mLow = low;
+ mHigh = high;
+ mException1 = exception1;
+ mException2 = exception1;
+ }
+ boolean matches(String zipCode) {
+ int prefix = Integer.parseInt(zipCode.substring(0, 2));
+ return (mLow <= prefix && prefix <= mHigh) || prefix == mException1
+ || prefix == mException2;
+ }
+ }
+
+ // Addresses consist of at least this many words, not including state and zip code.
+ private static final int MIN_ADDRESS_WORDS = 4;
+
+ // Adddresses consist of at most this many words, not including state and zip code.
+ private static final int MAX_ADDRESS_WORDS = 14;
+
+ // Addresses consist of at most this many lines.
+ private static final int MAX_ADDRESS_LINES = 5;
+
+ // No words in an address are longer than this many characters.
+ private static final int kMaxAddressNameWordLength = 25;
+
+ // Location name should be in the first MAX_LOCATION_NAME_DISTANCE words
+ private static final int MAX_LOCATION_NAME_DISTANCE = 5;
+
+ private static final ZipRange[] sStateZipCodeRanges = {
+ new ZipRange(99, 99, -1, -1), // AK Alaska.
+ new ZipRange(35, 36, -1, -1), // AL Alabama.
+ new ZipRange(71, 72, -1, -1), // AR Arkansas.
+ new ZipRange(96, 96, -1, -1), // AS American Samoa.
+ new ZipRange(85, 86, -1, -1), // AZ Arizona.
+ new ZipRange(90, 96, -1, -1), // CA California.
+ new ZipRange(80, 81, -1, -1), // CO Colorado.
+ new ZipRange(6, 6, -1, -1), // CT Connecticut.
+ new ZipRange(20, 20, -1, -1), // DC District of Columbia.
+ new ZipRange(19, 19, -1, -1), // DE Delaware.
+ new ZipRange(32, 34, -1, -1), // FL Florida.
+ new ZipRange(96, 96, -1, -1), // FM Federated States of Micronesia.
+ new ZipRange(30, 31, -1, -1), // GA Georgia.
+ new ZipRange(96, 96, -1, -1), // GU Guam.
+ new ZipRange(96, 96, -1, -1), // HI Hawaii.
+ new ZipRange(50, 52, -1, -1), // IA Iowa.
+ new ZipRange(83, 83, -1, -1), // ID Idaho.
+ new ZipRange(60, 62, -1, -1), // IL Illinois.
+ new ZipRange(46, 47, -1, -1), // IN Indiana.
+ new ZipRange(66, 67, 73, -1), // KS Kansas.
+ new ZipRange(40, 42, -1, -1), // KY Kentucky.
+ new ZipRange(70, 71, -1, -1), // LA Louisiana.
+ new ZipRange(1, 2, -1, -1), // MA Massachusetts.
+ new ZipRange(20, 21, -1, -1), // MD Maryland.
+ new ZipRange(3, 4, -1, -1), // ME Maine.
+ new ZipRange(96, 96, -1, -1), // MH Marshall Islands.
+ new ZipRange(48, 49, -1, -1), // MI Michigan.
+ new ZipRange(55, 56, -1, -1), // MN Minnesota.
+ new ZipRange(63, 65, -1, -1), // MO Missouri.
+ new ZipRange(96, 96, -1, -1), // MP Northern Mariana Islands.
+ new ZipRange(38, 39, -1, -1), // MS Mississippi.
+ new ZipRange(55, 56, -1, -1), // MT Montana.
+ new ZipRange(27, 28, -1, -1), // NC North Carolina.
+ new ZipRange(58, 58, -1, -1), // ND North Dakota.
+ new ZipRange(68, 69, -1, -1), // NE Nebraska.
+ new ZipRange(3, 4, -1, -1), // NH New Hampshire.
+ new ZipRange(7, 8, -1, -1), // NJ New Jersey.
+ new ZipRange(87, 88, 86, -1), // NM New Mexico.
+ new ZipRange(88, 89, 96, -1), // NV Nevada.
+ new ZipRange(10, 14, 0, 6), // NY New York.
+ new ZipRange(43, 45, -1, -1), // OH Ohio.
+ new ZipRange(73, 74, -1, -1), // OK Oklahoma.
+ new ZipRange(97, 97, -1, -1), // OR Oregon.
+ new ZipRange(15, 19, -1, -1), // PA Pennsylvania.
+ new ZipRange(6, 6, 0, 9), // PR Puerto Rico.
+ new ZipRange(96, 96, -1, -1), // PW Palau.
+ new ZipRange(2, 2, -1, -1), // RI Rhode Island.
+ new ZipRange(29, 29, -1, -1), // SC South Carolina.
+ new ZipRange(57, 57, -1, -1), // SD South Dakota.
+ new ZipRange(37, 38, -1, -1), // TN Tennessee.
+ new ZipRange(75, 79, 87, 88), // TX Texas.
+ new ZipRange(84, 84, -1, -1), // UT Utah.
+ new ZipRange(22, 24, 20, -1), // VA Virginia.
+ new ZipRange(6, 9, -1, -1), // VI Virgin Islands.
+ new ZipRange(5, 5, -1, -1), // VT Vermont.
+ new ZipRange(98, 99, -1, -1), // WA Washington.
+ new ZipRange(53, 54, -1, -1), // WI Wisconsin.
+ new ZipRange(24, 26, -1, -1), // WV West Virginia.
+ new ZipRange(82, 83, -1, -1) // WY Wyoming.
+ };
+
+ // Newlines
+ private static final String NL = "\n\u000B\u000C\r\u0085\u2028\u2029";
+
+ // Space characters
+ private static final String SP = "\u0009\u0020\u00A0\u1680\u2000\u2001"
+ + "\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F"
+ + "\u205F\u3000";
+
+ // Whitespace
+ private static final String WS = SP + NL;
+
+ // Characters that are considered word delimiters.
+ private static final String WORD_DELIM = ",*\u2022" + WS;
+
+ // Lookahead for word end.
+ private static final String WORD_END = "(?=[" + WORD_DELIM + "]|$)";
+
+ // Address words are a sequence of non-delimiter characters.
+ private static final Pattern sWordRe =
+ Pattern.compile("[^" + WORD_DELIM + "]+" + WORD_END, Pattern.CASE_INSENSITIVE);
+
+ // Characters that are considered suffix delimiters for house numbers.
+ private static final String HOUSE_POST_DELIM = ",\"'" + WS;
+
+ // Lookahead for house end.
+ private static final String HOUSE_END = "(?=[" + HOUSE_POST_DELIM + "]|$)";
+
+ // Characters that are considered prefix delimiters for house numbers.
+ private static final String HOUSE_PRE_DELIM = ":" + HOUSE_POST_DELIM;
+
+ // A house number component is "one" or a number, optionally
+ // followed by a single alphabetic character, or
+ private static final String HOUSE_COMPONENT = "(?:one|\\d+([a-z](?=[^a-z]|$)|st|nd|rd|th)?)";
+
+ // House numbers are a repetition of |HOUSE_COMPONENT|, separated by -, and followed by
+ // a delimiter character.
+ private static final Pattern sHouseNumberRe =
+ Pattern.compile(HOUSE_COMPONENT + "(?:-" + HOUSE_COMPONENT + ")*" + HOUSE_END,
+ Pattern.CASE_INSENSITIVE);
+
+ // XXX: do we want to accept whitespace other than 0x20 in state names?
+ private static final Pattern sStateRe = Pattern.compile("(?:"
+ + "(ak|alaska)|"
+ + "(al|alabama)|"
+ + "(ar|arkansas)|"
+ + "(as|american[" + SP + "]+samoa)|"
+ + "(az|arizona)|"
+ + "(ca|california)|"
+ + "(co|colorado)|"
+ + "(ct|connecticut)|"
+ + "(dc|district[" + SP + "]+of[" + SP + "]+columbia)|"
+ + "(de|delaware)|"
+ + "(fl|florida)|"
+ + "(fm|federated[" + SP + "]+states[" + SP + "]+of[" + SP + "]+micronesia)|"
+ + "(ga|georgia)|"
+ + "(gu|guam)|"
+ + "(hi|hawaii)|"
+ + "(ia|iowa)|"
+ + "(id|idaho)|"
+ + "(il|illinois)|"
+ + "(in|indiana)|"
+ + "(ks|kansas)|"
+ + "(ky|kentucky)|"
+ + "(la|louisiana)|"
+ + "(ma|massachusetts)|"
+ + "(md|maryland)|"
+ + "(me|maine)|"
+ + "(mh|marshall[" + SP + "]+islands)|"
+ + "(mi|michigan)|"
+ + "(mn|minnesota)|"
+ + "(mo|missouri)|"
+ + "(mp|northern[" + SP + "]+mariana[" + SP + "]+islands)|"
+ + "(ms|mississippi)|"
+ + "(mt|montana)|"
+ + "(nc|north[" + SP + "]+carolina)|"
+ + "(nd|north[" + SP + "]+dakota)|"
+ + "(ne|nebraska)|"
+ + "(nh|new[" + SP + "]+hampshire)|"
+ + "(nj|new[" + SP + "]+jersey)|"
+ + "(nm|new[" + SP + "]+mexico)|"
+ + "(nv|nevada)|"
+ + "(ny|new[" + SP + "]+york)|"
+ + "(oh|ohio)|"
+ + "(ok|oklahoma)|"
+ + "(or|oregon)|"
+ + "(pa|pennsylvania)|"
+ + "(pr|puerto[" + SP + "]+rico)|"
+ + "(pw|palau)|"
+ + "(ri|rhode[" + SP + "]+island)|"
+ + "(sc|south[" + SP + "]+carolina)|"
+ + "(sd|south[" + SP + "]+dakota)|"
+ + "(tn|tennessee)|"
+ + "(tx|texas)|"
+ + "(ut|utah)|"
+ + "(va|virginia)|"
+ + "(vi|virgin[" + SP + "]+islands)|"
+ + "(vt|vermont)|"
+ + "(wa|washington)|"
+ + "(wi|wisconsin)|"
+ + "(wv|west[" + SP + "]+virginia)|"
+ + "(wy|wyoming)"
+ + ")" + WORD_END,
+ Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern sLocationNameRe = Pattern.compile("(?:"
+ + "alley|annex|arcade|ave[.]?|avenue|alameda|bayou|"
+ + "beach|bend|bluffs?|bottom|boulevard|branch|bridge|"
+ + "brooks?|burgs?|bypass|broadway|camino|camp|canyon|"
+ + "cape|causeway|centers?|circles?|cliffs?|club|common|"
+ + "corners?|course|courts?|coves?|creek|crescent|crest|"
+ + "crossing|crossroad|curve|circulo|dale|dam|divide|"
+ + "drives?|estates?|expressway|extensions?|falls?|ferry|"
+ + "fields?|flats?|fords?|forest|forges?|forks?|fort|"
+ + "freeway|gardens?|gateway|glens?|greens?|groves?|"
+ + "harbors?|haven|heights|highway|hills?|hollow|inlet|"
+ + "islands?|isle|junctions?|keys?|knolls?|lakes?|land|"
+ + "landing|lane|lights?|loaf|locks?|lodge|loop|mall|"
+ + "manors?|meadows?|mews|mills?|mission|motorway|mount|"
+ + "mountains?|neck|orchard|oval|overpass|parks?|"
+ + "parkways?|pass|passage|path|pike|pines?|plains?|"
+ + "plaza|points?|ports?|prairie|privada|radial|ramp|"
+ + "ranch|rapids?|rd[.]?|rest|ridges?|river|roads?|route|"
+ + "row|rue|run|shoals?|shores?|skyway|springs?|spurs?|"
+ + "squares?|station|stravenue|stream|st[.]?|streets?|"
+ + "summit|speedway|terrace|throughway|trace|track|"
+ + "trafficway|trail|tunnel|turnpike|underpass|unions?|"
+ + "valleys?|viaduct|views?|villages?|ville|vista|walks?|"
+ + "wall|ways?|wells?|xing|xrd)" + WORD_END,
+ Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern sSuffixedNumberRe =
+ Pattern.compile("(\\d+)(st|nd|rd|th)", Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern sZipCodeRe =
+ Pattern.compile("(?:\\d{5}(?:-\\d{4})?)" + WORD_END, Pattern.CASE_INSENSITIVE);
+
+ private static boolean checkHouseNumber(String houseNumber) {
+ // Make sure that there are at most 5 digits.
+ int digitCount = 0;
+ for (int i = 0; i < houseNumber.length(); ++i) {
+ if (Character.isDigit(houseNumber.charAt(i))) ++digitCount;
+ }
+ if (digitCount > 5) return false;
+
+ // Make sure that any ordinals are valid.
+ Matcher suffixMatcher = sSuffixedNumberRe.matcher(houseNumber);
+ while (suffixMatcher.find()) {
+ int num = Integer.parseInt(suffixMatcher.group(1));
+ if (num == 0) {
+ return false; // 0th is invalid.
+ }
+ String suffix = suffixMatcher.group(2).toLowerCase(Locale.getDefault());
+ switch (num % 10) {
+ case 1:
+ return suffix.equals(num % 100 == 11 ? "th" : "st");
+ case 2:
+ return suffix.equals(num % 100 == 12 ? "th" : "nd");
+ case 3:
+ return suffix.equals(num % 100 == 13 ? "th" : "rd");
+ default:
+ return suffix.equals("th");
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Attempt to match a house number beginnning at position offset
+ * in content. The house number must be followed by a word
+ * delimiter or the end of the string, and if offset is non-zero,
+ * then it must also be preceded by a word delimiter.
+ *
+ * @return a MatchResult if a valid house number was found.
+ */
+ private static MatchResult matchHouseNumber(String content, int offset) {
+ if (offset > 0 && HOUSE_PRE_DELIM.indexOf(content.charAt(offset - 1)) == -1) return null;
+ Matcher matcher = sHouseNumberRe.matcher(content).region(offset, content.length());
+ if (matcher.lookingAt()) {
+ MatchResult matchResult = matcher.toMatchResult();
+ if (checkHouseNumber(matchResult.group(0))) return matchResult;
+ }
+ return null;
+ }
+
+ /**
+ * Attempt to match a US state beginnning at position offset in
+ * content. The matching state must be followed by a word
+ * delimiter or the end of the string, and if offset is non-zero,
+ * then it must also be preceded by a word delimiter.
+ *
+ * @return a MatchResult if a valid US state (or two letter code)
+ * was found.
+ */
+ private static MatchResult matchState(String content, int offset) {
+ if (offset > 0 && WORD_DELIM.indexOf(content.charAt(offset - 1)) == -1) return null;
+ Matcher stateMatcher = sStateRe.matcher(content).region(offset, content.length());
+ return stateMatcher.lookingAt() ? stateMatcher.toMatchResult() : null;
+ }
+
+ /**
+ * Test whether zipCode matches the U.S. zip code format (ddddd or
+ * ddddd-dddd) and is within the expected range, given that
+ * stateMatch is a match of sStateRe.
+ *
+ * @return true if zipCode is a valid zip code, is legal for the
+ * matched state, and is followed by a word delimiter or the end
+ * of the string.
+ */
+ private static boolean isValidZipCode(String zipCode, MatchResult stateMatch) {
+ if (stateMatch == null) return false;
+ // Work out the index of the state, based on which group matched.
+ int stateIndex = stateMatch.groupCount();
+ while (stateIndex > 0) {
+ if (stateMatch.group(stateIndex--) != null) break;
+ }
+ return sZipCodeRe.matcher(zipCode).matches()
+ && sStateZipCodeRanges[stateIndex].matches(zipCode);
+ }
+
+ /**
+ * Test whether location is one of the valid locations.
+ *
+ * @return true if location starts with a valid location name
+ * followed by a word delimiter or the end of the string.
+ */
+ private static boolean isValidLocationName(String location) {
+ return sLocationNameRe.matcher(location).matches();
+ }
+
+ /**
+ * Attempt to match a complete address in content, starting with
+ * houseNumberMatch.
+ *
+ * @param content The string to search.
+ * @param houseNumberMatch A matching house number to start extending.
+ * @return +ve: the end of the match
+ * +ve: the position to restart searching for house numbers, negated.
+ */
+ private static int attemptMatch(String content, MatchResult houseNumberMatch) {
+ int restartPos = -1;
+ int nonZipMatch = -1;
+ int it = houseNumberMatch.end();
+ int numLines = 1;
+ boolean consecutiveHouseNumbers = true;
+ boolean foundLocationName = false;
+ int wordCount = 1;
+ String lastWord = "";
+
+ Matcher matcher = sWordRe.matcher(content);
+
+ for (; it < content.length(); lastWord = matcher.group(0), it = matcher.end()) {
+ if (!matcher.find(it)) {
+ // No more words in the input sequence.
+ return -content.length();
+ }
+ if (matcher.end() - matcher.start() > kMaxAddressNameWordLength) {
+ // Word is too long to be part of an address. Fail.
+ return -matcher.end();
+ }
+
+ // Count the number of newlines we just consumed.
+ while (it < matcher.start()) {
+ if (NL.indexOf(content.charAt(it++)) != -1) ++numLines;
+ }
+
+ // Consumed too many lines. Fail.
+ if (numLines > MAX_ADDRESS_LINES) break;
+
+ // Consumed too many words. Fail.
+ if (++wordCount > MAX_ADDRESS_WORDS) break;
+
+ if (matchHouseNumber(content, it) != null) {
+ if (consecutiveHouseNumbers && numLines > 1) {
+ // Last line ended with a number, and this this line starts with one.
+ // Restart at this number.
+ return -it;
+ }
+ // Remember the position of this match as the restart position.
+ if (restartPos == -1) restartPos = it;
+ continue;
+ }
+
+ consecutiveHouseNumbers = false;
+
+ if (isValidLocationName(matcher.group(0))) {
+ foundLocationName = true;
+ continue;
+ }
+
+ if (wordCount == MAX_LOCATION_NAME_DISTANCE && !foundLocationName) {
+ // Didn't find a location name in time. Fail.
+ it = matcher.end();
+ break;
+ }
+
+ if (foundLocationName && wordCount > MIN_ADDRESS_WORDS) {
+ // We can now attempt to match a state.
+ MatchResult stateMatch = matchState(content, it);
+ if (stateMatch != null) {
+ if (lastWord.equals("et") && stateMatch.group(0).equals("al")) {
+ // Reject "et al" as a false postitive.
+ it = stateMatch.end();
+ break;
+ }
+
+ // At this point we've matched a state; try to match a zip code after it.
+ Matcher zipMatcher = sWordRe.matcher(content);
+ if (zipMatcher.find(stateMatch.end())
+ && isValidZipCode(zipMatcher.group(0), stateMatch)) {
+ return zipMatcher.end();
+ }
+ // The content ends with a state but no zip
+ // code. This is a legal match according to the
+ // documentation. N.B. This differs from the
+ // original c++ implementation, which only allowed
+ // the zip code to be optional at the end of the
+ // string, which presumably is a bug. Now we
+ // prefer to find a match with a zip code, but
+ // remember non-zip matches and return them if
+ // necessary.
+ nonZipMatch = stateMatch.end();
+ }
+ }
+ }
+
+ if (nonZipMatch > 0) return nonZipMatch;
+
+ return -(restartPos > 0 ? restartPos : it);
+ }
+
+ /**
+ * Return the first matching address in content.
+ *
+ * @param content The string to search.
+ * @return The first valid address, or null if no address was matched.
+ */
+ static String findAddress(String content) {
+ Matcher houseNumberMatcher = sHouseNumberRe.matcher(content);
+ int start = 0;
+ while (houseNumberMatcher.find(start)) {
+ if (checkHouseNumber(houseNumberMatcher.group(0))) {
+ start = houseNumberMatcher.start();
+ int end = attemptMatch(content, houseNumberMatcher);
+ if (end > 0) {
+ return content.substring(start, end);
+ }
+ start = -end;
+ } else {
+ start = houseNumberMatcher.end();
+ }
+ }
+ return null;
+ }
+}
diff --git a/android/webkit/SafeBrowsingResponse.java b/android/webkit/SafeBrowsingResponse.java
index 960b56b..1d3a617 100644
--- a/android/webkit/SafeBrowsingResponse.java
+++ b/android/webkit/SafeBrowsingResponse.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 The Android Open Source Project
+ * Copyright (C) 2017 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.
@@ -16,5 +16,36 @@
package android.webkit;
-public class SafeBrowsingResponse {
+/**
+ * Used to indicate an action to take when hitting a malicious URL. Instances of this class are
+ * created by the WebView and passed to {@link android.webkit.WebViewClient#onSafeBrowsingHit}. The
+ * host application must call {@link #showInterstitial(boolean)}, {@link #proceed(boolean)}, or
+ * {@link #backToSafety(boolean)} to set the WebView's response to the Safe Browsing hit.
+ *
+ * <p>
+ * If reporting is enabled, all reports will be sent according to the privacy policy referenced by
+ * {@link android.webkit.WebView#getSafeBrowsingPrivacyPolicyUrl()}.
+ */
+public abstract class SafeBrowsingResponse {
+
+ /**
+ * Display the default interstitial.
+ *
+ * @param allowReporting {@code true} if the interstitial should show a reporting checkbox.
+ */
+ public abstract void showInterstitial(boolean allowReporting);
+
+ /**
+ * Act as if the user clicked "visit this unsafe site."
+ *
+ * @param report {@code true} to enable Safe Browsing reporting.
+ */
+ public abstract void proceed(boolean report);
+
+ /**
+ * Act as if the user clicked "back to safety."
+ *
+ * @param report {@code true} to enable Safe Browsing reporting.
+ */
+ public abstract void backToSafety(boolean report);
}
diff --git a/android/webkit/WebViewClient.java b/android/webkit/WebViewClient.java
index f5d220c..d0f9eee 100644
--- a/android/webkit/WebViewClient.java
+++ b/android/webkit/WebViewClient.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 The Android Open Source Project
+ * 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.
@@ -13,7 +13,545 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package android.webkit;
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.graphics.Bitmap;
+import android.net.http.SslError;
+import android.os.Message;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.ViewRootImpl;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
public class WebViewClient {
+
+ /**
+ * Give the host application a chance to take over the control when a new
+ * url is about to be loaded in the current WebView. If WebViewClient is not
+ * provided, by default WebView will ask Activity Manager to choose the
+ * proper handler for the url. If WebViewClient is provided, return {@code true}
+ * means the host application handles the url, while return {@code false} means the
+ * current WebView handles the url.
+ * This method is not called for requests using the POST "method".
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param url The url to be loaded.
+ * @return {@code true} if the host application wants to leave the current WebView
+ * and handle the url itself, otherwise return {@code false}.
+ * @deprecated Use {@link #shouldOverrideUrlLoading(WebView, WebResourceRequest)
+ * shouldOverrideUrlLoading(WebView, WebResourceRequest)} instead.
+ */
+ @Deprecated
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ return false;
+ }
+
+ /**
+ * Give the host application a chance to take over the control when a new
+ * url is about to be loaded in the current WebView. If WebViewClient is not
+ * provided, by default WebView will ask Activity Manager to choose the
+ * proper handler for the url. If WebViewClient is provided, return {@code true}
+ * means the host application handles the url, while return {@code false} means the
+ * current WebView handles the url.
+ *
+ * <p>Notes:
+ * <ul>
+ * <li>This method is not called for requests using the POST "method".</li>
+ * <li>This method is also called for subframes with non-http schemes, thus it is
+ * strongly disadvised to unconditionally call {@link WebView#loadUrl(String)}
+ * with the request's url from inside the method and then return {@code true},
+ * as this will make WebView to attempt loading a non-http url, and thus fail.</li>
+ * </ul>
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param request Object containing the details of the request.
+ * @return {@code true} if the host application wants to leave the current WebView
+ * and handle the url itself, otherwise return {@code false}.
+ */
+ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
+ return shouldOverrideUrlLoading(view, request.getUrl().toString());
+ }
+
+ /**
+ * Notify the host application that a page has started loading. This method
+ * is called once for each main frame load so a page with iframes or
+ * framesets will call onPageStarted one time for the main frame. This also
+ * means that onPageStarted will not be called when the contents of an
+ * embedded frame changes, i.e. clicking a link whose target is an iframe,
+ * it will also not be called for fragment navigations (navigations to
+ * #fragment_id).
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param url The url to be loaded.
+ * @param favicon The favicon for this page if it already exists in the
+ * database.
+ */
+ public void onPageStarted(WebView view, String url, Bitmap favicon) {
+ }
+
+ /**
+ * Notify the host application that a page has finished loading. This method
+ * is called only for main frame. When onPageFinished() is called, the
+ * rendering picture may not be updated yet. To get the notification for the
+ * new Picture, use {@link WebView.PictureListener#onNewPicture}.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param url The url of the page.
+ */
+ public void onPageFinished(WebView view, String url) {
+ }
+
+ /**
+ * Notify the host application that the WebView will load the resource
+ * specified by the given url.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param url The url of the resource the WebView will load.
+ */
+ public void onLoadResource(WebView view, String url) {
+ }
+
+ /**
+ * Notify the host application that {@link android.webkit.WebView} content left over from
+ * previous page navigations will no longer be drawn.
+ *
+ * <p>This callback can be used to determine the point at which it is safe to make a recycled
+ * {@link android.webkit.WebView} visible, ensuring that no stale content is shown. It is called
+ * at the earliest point at which it can be guaranteed that {@link WebView#onDraw} will no
+ * longer draw any content from previous navigations. The next draw will display either the
+ * {@link WebView#setBackgroundColor background color} of the {@link WebView}, or some of the
+ * contents of the newly loaded page.
+ *
+ * <p>This method is called when the body of the HTTP response has started loading, is reflected
+ * in the DOM, and will be visible in subsequent draws. This callback occurs early in the
+ * document loading process, and as such you should expect that linked resources (for example,
+ * CSS and images) may not be available.
+ *
+ * <p>For more fine-grained notification of visual state updates, see {@link
+ * WebView#postVisualStateCallback}.
+ *
+ * <p>Please note that all the conditions and recommendations applicable to
+ * {@link WebView#postVisualStateCallback} also apply to this API.
+ *
+ * <p>This callback is only called for main frame navigations.
+ *
+ * @param view The {@link android.webkit.WebView} for which the navigation occurred.
+ * @param url The URL corresponding to the page navigation that triggered this callback.
+ */
+ public void onPageCommitVisible(WebView view, String url) {
+ }
+
+ /**
+ * Notify the host application of a resource request and allow the
+ * application to return the data. If the return value is {@code null}, the WebView
+ * will continue to load the resource as usual. Otherwise, the return
+ * response and data will be used.
+ *
+ * <p class="note"><b>Note:</b> This method is called on a thread
+ * other than the UI thread so clients should exercise caution
+ * when accessing private data or the view system.
+ *
+ * <p class="note"><b>Note:</b> When Safe Browsing is enabled, these URLs still undergo Safe
+ * Browsing checks. If this is undesired, whitelist the URL with {@link
+ * WebView#setSafeBrowsingWhitelist} or ignore the warning with {@link #onSafeBrowsingHit}.
+ *
+ * @param view The {@link android.webkit.WebView} that is requesting the
+ * resource.
+ * @param url The raw url of the resource.
+ * @return A {@link android.webkit.WebResourceResponse} containing the
+ * response information or {@code null} if the WebView should load the
+ * resource itself.
+ * @deprecated Use {@link #shouldInterceptRequest(WebView, WebResourceRequest)
+ * shouldInterceptRequest(WebView, WebResourceRequest)} instead.
+ */
+ @Deprecated
+ @Nullable
+ public WebResourceResponse shouldInterceptRequest(WebView view,
+ String url) {
+ return null;
+ }
+
+ /**
+ * Notify the host application of a resource request and allow the
+ * application to return the data. If the return value is {@code null}, the WebView
+ * will continue to load the resource as usual. Otherwise, the return
+ * response and data will be used.
+ *
+ * <p class="note"><b>Note:</b> This method is called on a thread
+ * other than the UI thread so clients should exercise caution
+ * when accessing private data or the view system.
+ *
+ * <p class="note"><b>Note:</b> When Safe Browsing is enabled, these URLs still undergo Safe
+ * Browsing checks. If this is undesired, whitelist the URL with {@link
+ * WebView#setSafeBrowsingWhitelist} or ignore the warning with {@link #onSafeBrowsingHit}.
+ *
+ * @param view The {@link android.webkit.WebView} that is requesting the
+ * resource.
+ * @param request Object containing the details of the request.
+ * @return A {@link android.webkit.WebResourceResponse} containing the
+ * response information or {@code null} if the WebView should load the
+ * resource itself.
+ */
+ @Nullable
+ public WebResourceResponse shouldInterceptRequest(WebView view,
+ WebResourceRequest request) {
+ return shouldInterceptRequest(view, request.getUrl().toString());
+ }
+
+ /**
+ * Notify the host application that there have been an excessive number of
+ * HTTP redirects. As the host application if it would like to continue
+ * trying to load the resource. The default behavior is to send the cancel
+ * message.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param cancelMsg The message to send if the host wants to cancel
+ * @param continueMsg The message to send if the host wants to continue
+ * @deprecated This method is no longer called. When the WebView encounters
+ * a redirect loop, it will cancel the load.
+ */
+ @Deprecated
+ public void onTooManyRedirects(WebView view, Message cancelMsg,
+ Message continueMsg) {
+ cancelMsg.sendToTarget();
+ }
+
+ // These ints must match up to the hidden values in EventHandler.
+ /** Generic error */
+ public static final int ERROR_UNKNOWN = -1;
+ /** Server or proxy hostname lookup failed */
+ public static final int ERROR_HOST_LOOKUP = -2;
+ /** Unsupported authentication scheme (not basic or digest) */
+ public static final int ERROR_UNSUPPORTED_AUTH_SCHEME = -3;
+ /** User authentication failed on server */
+ public static final int ERROR_AUTHENTICATION = -4;
+ /** User authentication failed on proxy */
+ public static final int ERROR_PROXY_AUTHENTICATION = -5;
+ /** Failed to connect to the server */
+ public static final int ERROR_CONNECT = -6;
+ /** Failed to read or write to the server */
+ public static final int ERROR_IO = -7;
+ /** Connection timed out */
+ public static final int ERROR_TIMEOUT = -8;
+ /** Too many redirects */
+ public static final int ERROR_REDIRECT_LOOP = -9;
+ /** Unsupported URI scheme */
+ public static final int ERROR_UNSUPPORTED_SCHEME = -10;
+ /** Failed to perform SSL handshake */
+ public static final int ERROR_FAILED_SSL_HANDSHAKE = -11;
+ /** Malformed URL */
+ public static final int ERROR_BAD_URL = -12;
+ /** Generic file error */
+ public static final int ERROR_FILE = -13;
+ /** File not found */
+ public static final int ERROR_FILE_NOT_FOUND = -14;
+ /** Too many requests during this load */
+ public static final int ERROR_TOO_MANY_REQUESTS = -15;
+ /** Resource load was canceled by Safe Browsing */
+ public static final int ERROR_UNSAFE_RESOURCE = -16;
+
+ /** @hide */
+ @IntDef(prefix = { "SAFE_BROWSING_THREAT_" }, value = {
+ SAFE_BROWSING_THREAT_UNKNOWN,
+ SAFE_BROWSING_THREAT_MALWARE,
+ SAFE_BROWSING_THREAT_PHISHING,
+ SAFE_BROWSING_THREAT_UNWANTED_SOFTWARE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SafeBrowsingThreat {}
+
+ /** The resource was blocked for an unknown reason */
+ public static final int SAFE_BROWSING_THREAT_UNKNOWN = 0;
+ /** The resource was blocked because it contains malware */
+ public static final int SAFE_BROWSING_THREAT_MALWARE = 1;
+ /** The resource was blocked because it contains deceptive content */
+ public static final int SAFE_BROWSING_THREAT_PHISHING = 2;
+ /** The resource was blocked because it contains unwanted software */
+ public static final int SAFE_BROWSING_THREAT_UNWANTED_SOFTWARE = 3;
+
+ /**
+ * Report an error to the host application. These errors are unrecoverable
+ * (i.e. the main resource is unavailable). The {@code errorCode} parameter
+ * corresponds to one of the {@code ERROR_*} constants.
+ * @param view The WebView that is initiating the callback.
+ * @param errorCode The error code corresponding to an ERROR_* value.
+ * @param description A String describing the error.
+ * @param failingUrl The url that failed to load.
+ * @deprecated Use {@link #onReceivedError(WebView, WebResourceRequest, WebResourceError)
+ * onReceivedError(WebView, WebResourceRequest, WebResourceError)} instead.
+ */
+ @Deprecated
+ public void onReceivedError(WebView view, int errorCode,
+ String description, String failingUrl) {
+ }
+
+ /**
+ * Report web resource loading error to the host application. These errors usually indicate
+ * inability to connect to the server. Note that unlike the deprecated version of the callback,
+ * the new version will be called for any resource (iframe, image, etc.), not just for the main
+ * page. Thus, it is recommended to perform minimum required work in this callback.
+ * @param view The WebView that is initiating the callback.
+ * @param request The originating request.
+ * @param error Information about the error occurred.
+ */
+ public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
+ if (request.isForMainFrame()) {
+ onReceivedError(view,
+ error.getErrorCode(), error.getDescription().toString(),
+ request.getUrl().toString());
+ }
+ }
+
+ /**
+ * Notify the host application that an HTTP error has been received from the server while
+ * loading a resource. HTTP errors have status codes >= 400. This callback will be called
+ * for any resource (iframe, image, etc.), not just for the main page. Thus, it is recommended
+ * to perform minimum required work in this callback. Note that the content of the server
+ * response may not be provided within the {@code errorResponse} parameter.
+ * @param view The WebView that is initiating the callback.
+ * @param request The originating request.
+ * @param errorResponse Information about the error occurred.
+ */
+ public void onReceivedHttpError(
+ WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
+ }
+
+ /**
+ * As the host application if the browser should resend data as the
+ * requested page was a result of a POST. The default is to not resend the
+ * data.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param dontResend The message to send if the browser should not resend
+ * @param resend The message to send if the browser should resend data
+ */
+ public void onFormResubmission(WebView view, Message dontResend,
+ Message resend) {
+ dontResend.sendToTarget();
+ }
+
+ /**
+ * Notify the host application to update its visited links database.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param url The url being visited.
+ * @param isReload {@code true} if this url is being reloaded.
+ */
+ public void doUpdateVisitedHistory(WebView view, String url,
+ boolean isReload) {
+ }
+
+ /**
+ * Notify the host application that an SSL error occurred while loading a
+ * resource. The host application must call either handler.cancel() or
+ * handler.proceed(). Note that the decision may be retained for use in
+ * response to future SSL errors. The default behavior is to cancel the
+ * load.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param handler An SslErrorHandler object that will handle the user's
+ * response.
+ * @param error The SSL error object.
+ */
+ public void onReceivedSslError(WebView view, SslErrorHandler handler,
+ SslError error) {
+ handler.cancel();
+ }
+
+ /**
+ * Notify the host application to handle a SSL client certificate request. The host application
+ * is responsible for showing the UI if desired and providing the keys. There are three ways to
+ * respond: {@link ClientCertRequest#proceed}, {@link ClientCertRequest#cancel}, or {@link
+ * ClientCertRequest#ignore}. Webview stores the response in memory (for the life of the
+ * application) if {@link ClientCertRequest#proceed} or {@link ClientCertRequest#cancel} is
+ * called and does not call {@code onReceivedClientCertRequest()} again for the same host and
+ * port pair. Webview does not store the response if {@link ClientCertRequest#ignore}
+ * is called. Note that, multiple layers in chromium network stack might be
+ * caching the responses, so the behavior for ignore is only a best case
+ * effort.
+ *
+ * This method is called on the UI thread. During the callback, the
+ * connection is suspended.
+ *
+ * For most use cases, the application program should implement the
+ * {@link android.security.KeyChainAliasCallback} interface and pass it to
+ * {@link android.security.KeyChain#choosePrivateKeyAlias} to start an
+ * activity for the user to choose the proper alias. The keychain activity will
+ * provide the alias through the callback method in the implemented interface. Next
+ * the application should create an async task to call
+ * {@link android.security.KeyChain#getPrivateKey} to receive the key.
+ *
+ * An example implementation of client certificates can be seen at
+ * <A href="https://android.googlesource.com/platform/packages/apps/Browser/+/android-5.1.1_r1/src/com/android/browser/Tab.java">
+ * AOSP Browser</a>
+ *
+ * The default behavior is to cancel, returning no client certificate.
+ *
+ * @param view The WebView that is initiating the callback
+ * @param request An instance of a {@link ClientCertRequest}
+ *
+ */
+ public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {
+ request.cancel();
+ }
+
+ /**
+ * Notifies the host application that the WebView received an HTTP
+ * authentication request. The host application can use the supplied
+ * {@link HttpAuthHandler} to set the WebView's response to the request.
+ * The default behavior is to cancel the request.
+ *
+ * @param view the WebView that is initiating the callback
+ * @param handler the HttpAuthHandler used to set the WebView's response
+ * @param host the host requiring authentication
+ * @param realm the realm for which authentication is required
+ * @see WebView#getHttpAuthUsernamePassword
+ */
+ public void onReceivedHttpAuthRequest(WebView view,
+ HttpAuthHandler handler, String host, String realm) {
+ handler.cancel();
+ }
+
+ /**
+ * Give the host application a chance to handle the key event synchronously.
+ * e.g. menu shortcut key events need to be filtered this way. If return
+ * true, WebView will not handle the key event. If return {@code false}, WebView
+ * will always handle the key event, so none of the super in the view chain
+ * will see the key event. The default behavior returns {@code false}.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param event The key event.
+ * @return {@code true} if the host application wants to handle the key event
+ * itself, otherwise return {@code false}
+ */
+ public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Notify the host application that a key was not handled by the WebView.
+ * Except system keys, WebView always consumes the keys in the normal flow
+ * or if {@link #shouldOverrideKeyEvent} returns {@code true}. This is called asynchronously
+ * from where the key is dispatched. It gives the host application a chance
+ * to handle the unhandled key events.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param event The key event.
+ */
+ public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
+ onUnhandledInputEventInternal(view, event);
+ }
+
+ /**
+ * Notify the host application that a input event was not handled by the WebView.
+ * Except system keys, WebView always consumes input events in the normal flow
+ * or if {@link #shouldOverrideKeyEvent} returns {@code true}. This is called asynchronously
+ * from where the event is dispatched. It gives the host application a chance
+ * to handle the unhandled input events.
+ *
+ * Note that if the event is a {@link android.view.MotionEvent}, then it's lifetime is only
+ * that of the function call. If the WebViewClient wishes to use the event beyond that, then it
+ * <i>must</i> create a copy of the event.
+ *
+ * It is the responsibility of overriders of this method to call
+ * {@link #onUnhandledKeyEvent(WebView, KeyEvent)}
+ * when appropriate if they wish to continue receiving events through it.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param event The input event.
+ * @removed
+ */
+ public void onUnhandledInputEvent(WebView view, InputEvent event) {
+ if (event instanceof KeyEvent) {
+ onUnhandledKeyEvent(view, (KeyEvent) event);
+ return;
+ }
+ onUnhandledInputEventInternal(view, event);
+ }
+
+ private void onUnhandledInputEventInternal(WebView view, InputEvent event) {
+ ViewRootImpl root = view.getViewRootImpl();
+ if (root != null) {
+ root.dispatchUnhandledInputEvent(event);
+ }
+ }
+
+ /**
+ * Notify the host application that the scale applied to the WebView has
+ * changed.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param oldScale The old scale factor
+ * @param newScale The new scale factor
+ */
+ public void onScaleChanged(WebView view, float oldScale, float newScale) {
+ }
+
+ /**
+ * Notify the host application that a request to automatically log in the
+ * user has been processed.
+ * @param view The WebView requesting the login.
+ * @param realm The account realm used to look up accounts.
+ * @param account An optional account. If not {@code null}, the account should be
+ * checked against accounts on the device. If it is a valid
+ * account, it should be used to log in the user.
+ * @param args Authenticator specific arguments used to log in the user.
+ */
+ public void onReceivedLoginRequest(WebView view, String realm,
+ @Nullable String account, String args) {
+ }
+
+ /**
+ * Notify host application that the given WebView's render process has exited.
+ *
+ * Multiple WebView instances may be associated with a single render process;
+ * onRenderProcessGone will be called for each WebView that was affected.
+ * The application's implementation of this callback should only attempt to
+ * clean up the specific WebView given as a parameter, and should not assume
+ * that other WebView instances are affected.
+ *
+ * The given WebView can't be used, and should be removed from the view hierarchy,
+ * all references to it should be cleaned up, e.g any references in the Activity
+ * or other classes saved using {@link android.view.View#findViewById} and similar calls, etc.
+ *
+ * To cause an render process crash for test purpose, the application can
+ * call {@code loadUrl("chrome://crash")} on the WebView. Note that multiple WebView
+ * instances may be affected if they share a render process, not just the
+ * specific WebView which loaded chrome://crash.
+ *
+ * @param view The WebView which needs to be cleaned up.
+ * @param detail the reason why it exited.
+ * @return {@code true} if the host application handled the situation that process has
+ * exited, otherwise, application will crash if render process crashed,
+ * or be killed if render process was killed by the system.
+ */
+ public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) {
+ return false;
+ }
+
+ /**
+ * Notify the host application that a loading URL has been flagged by Safe Browsing.
+ *
+ * The application must invoke the callback to indicate the preferred response. The default
+ * behavior is to show an interstitial to the user, with the reporting checkbox visible.
+ *
+ * If the application needs to show its own custom interstitial UI, the callback can be invoked
+ * asynchronously with {@link SafeBrowsingResponse#backToSafety} or {@link
+ * SafeBrowsingResponse#proceed}, depending on user response.
+ *
+ * @param view The WebView that hit the malicious resource.
+ * @param request Object containing the details of the request.
+ * @param threatType The reason the resource was caught by Safe Browsing, corresponding to a
+ * {@code SAFE_BROWSING_THREAT_*} value.
+ * @param callback Applications must invoke one of the callback methods.
+ */
+ public void onSafeBrowsingHit(WebView view, WebResourceRequest request,
+ @SafeBrowsingThreat int threatType, SafeBrowsingResponse callback) {
+ callback.showInterstitial(/* allowReporting */ true);
+ }
}
diff --git a/android/webkit/WebViewFactory.java b/android/webkit/WebViewFactory.java
index b3522ec..e9fe481 100644
--- a/android/webkit/WebViewFactory.java
+++ b/android/webkit/WebViewFactory.java
@@ -27,7 +27,6 @@
import android.content.pm.Signature;
import android.os.RemoteException;
import android.os.ServiceManager;
-import android.os.StrictMode;
import android.os.Trace;
import android.util.AndroidRuntimeException;
import android.util.ArraySet;
@@ -251,7 +250,6 @@
"WebView.disableWebView() was called: WebView is disabled");
}
- StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getProvider()");
try {
Class<WebViewFactoryProvider> providerClass = getProviderClass();
@@ -279,7 +277,6 @@
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
- StrictMode.setThreadPolicy(oldPolicy);
}
}
}
diff --git a/android/webkit/WebViewFactoryProvider.java b/android/webkit/WebViewFactoryProvider.java
index 3ced6a5..4f7cdab 100644
--- a/android/webkit/WebViewFactoryProvider.java
+++ b/android/webkit/WebViewFactoryProvider.java
@@ -172,4 +172,10 @@
* @return the singleton WebViewDatabase instance
*/
WebViewDatabase getWebViewDatabase(Context context);
+
+ /**
+ * Gets the classloader used to load internal WebView implementation classes. This interface
+ * should only be used by the WebView Support Library.
+ */
+ ClassLoader getWebViewClassLoader();
}
diff --git a/android/widget/AbsListView.java b/android/widget/AbsListView.java
index e0c897d..594d240 100644
--- a/android/widget/AbsListView.java
+++ b/android/widget/AbsListView.java
@@ -19,6 +19,7 @@
import android.annotation.ColorInt;
import android.annotation.DrawableRes;
import android.annotation.NonNull;
+import android.annotation.TestApi;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
@@ -30,6 +31,7 @@
import android.os.Bundle;
import android.os.Debug;
import android.os.Handler;
+import android.os.LocaleList;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.StrictMode;
@@ -2744,7 +2746,7 @@
}
private void drawSelector(Canvas canvas) {
- if (!mSelectorRect.isEmpty()) {
+ if (shouldDrawSelector()) {
final Drawable selector = mSelector;
selector.setBounds(mSelectorRect);
selector.draw(canvas);
@@ -2752,6 +2754,14 @@
}
/**
+ * @hide
+ */
+ @TestApi
+ public final boolean shouldDrawSelector() {
+ return !mSelectorRect.isEmpty();
+ }
+
+ /**
* Controls whether the selection highlight drawable should be drawn on top of the item or
* behind it.
*
@@ -6026,6 +6036,11 @@
public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
return getTarget().commitContent(inputContentInfo, flags, opts);
}
+
+ @Override
+ public void reportLanguageHint(@NonNull LocaleList languageHint) {
+ getTarget().reportLanguageHint(languageHint);
+ }
}
/**
@@ -6849,7 +6864,7 @@
// detached and we do not allow detached views to fire accessibility
// events. So we are announcing that the subtree changed giving a chance
// to clients holding on to a view in this subtree to refresh it.
- notifyViewAccessibilityStateChangedIfNeeded(
+ notifyAccessibilityStateChanged(
AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
// Don't scrap views that have transient state.
diff --git a/android/widget/AdapterView.java b/android/widget/AdapterView.java
index 6c19256..08374cb 100644
--- a/android/widget/AdapterView.java
+++ b/android/widget/AdapterView.java
@@ -1093,7 +1093,7 @@
checkSelectionChanged();
}
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
/**
diff --git a/android/widget/CheckedTextView.java b/android/widget/CheckedTextView.java
index 92bfd56..af01a3e 100644
--- a/android/widget/CheckedTextView.java
+++ b/android/widget/CheckedTextView.java
@@ -132,7 +132,7 @@
if (mChecked != checked) {
mChecked = checked;
refreshDrawableState();
- notifyViewAccessibilityStateChangedIfNeeded(
+ notifyAccessibilityStateChanged(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
}
}
diff --git a/android/widget/CompoundButton.java b/android/widget/CompoundButton.java
index 0762b15..e57f153 100644
--- a/android/widget/CompoundButton.java
+++ b/android/widget/CompoundButton.java
@@ -158,7 +158,7 @@
mCheckedFromResource = false;
mChecked = checked;
refreshDrawableState();
- notifyViewAccessibilityStateChangedIfNeeded(
+ notifyAccessibilityStateChanged(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
// Avoid infinite recursions if setChecked() is called from a listener
diff --git a/android/widget/EditText.java b/android/widget/EditText.java
index 336c20c..728824c 100644
--- a/android/widget/EditText.java
+++ b/android/widget/EditText.java
@@ -106,6 +106,10 @@
@Override
public Editable getText() {
CharSequence text = super.getText();
+ // This can only happen during construction.
+ if (text == null) {
+ return null;
+ }
if (text instanceof Editable) {
return (Editable) super.getText();
}
diff --git a/android/widget/Editor.java b/android/widget/Editor.java
index 05d18d1..7bb0db1 100644
--- a/android/widget/Editor.java
+++ b/android/widget/Editor.java
@@ -2095,14 +2095,7 @@
if (!(mTextView.getText() instanceof Spannable)) {
return;
}
- Spannable text = (Spannable) mTextView.getText();
stopTextActionMode();
- if (mTextView.isTextSelectable()) {
- Selection.setSelection((Spannable) text, link.getStart(), link.getEnd());
- } else {
- //TODO: Nonselectable text
- }
-
getSelectionActionModeHelper().startLinkActionModeAsync(link);
}
@@ -2179,7 +2172,8 @@
return false;
}
- if (!checkField() || !mTextView.hasSelection()) {
+ if (actionMode != TextActionMode.TEXT_LINK
+ && (!checkField() || !mTextView.hasSelection())) {
return false;
}
@@ -3679,6 +3673,8 @@
mIsShowingUp = true;
super.show();
}
+
+ mSuggestionListView.setVisibility(mNumberOfSuggestions == 0 ? View.GONE : View.VISIBLE);
}
@Override
@@ -4012,7 +4008,6 @@
if (isValidAssistMenuItem(
textClassification.getIcon(),
textClassification.getLabel(),
- textClassification.getOnClickListener(),
textClassification.getIntent())) {
final MenuItem item = menu.add(
TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST,
@@ -4020,14 +4015,15 @@
.setIcon(textClassification.getIcon())
.setIntent(textClassification.getIntent());
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
- mAssistClickHandlers.put(item, textClassification.getOnClickListener());
+ mAssistClickHandlers.put(
+ item, TextClassification.createStartActivityOnClickListener(
+ mTextView.getContext(), textClassification.getIntent()));
}
final int count = textClassification.getSecondaryActionsCount();
for (int i = 0; i < count; i++) {
if (!isValidAssistMenuItem(
textClassification.getSecondaryIcon(i),
textClassification.getSecondaryLabel(i),
- textClassification.getSecondaryOnClickListener(i),
textClassification.getSecondaryIntent(i))) {
continue;
}
@@ -4038,7 +4034,9 @@
.setIcon(textClassification.getSecondaryIcon(i))
.setIntent(textClassification.getSecondaryIntent(i));
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
- mAssistClickHandlers.put(item, textClassification.getSecondaryOnClickListener(i));
+ mAssistClickHandlers.put(item,
+ TextClassification.createStartActivityOnClickListener(
+ mTextView.getContext(), textClassification.getSecondaryIntent(i)));
}
}
@@ -4054,10 +4052,9 @@
}
}
- private boolean isValidAssistMenuItem(
- Drawable icon, CharSequence label, OnClickListener onClick, Intent intent) {
+ private boolean isValidAssistMenuItem(Drawable icon, CharSequence label, Intent intent) {
final boolean hasUi = icon != null || !TextUtils.isEmpty(label);
- final boolean hasAction = onClick != null || isSupportedIntent(intent);
+ final boolean hasAction = isSupportedIntent(intent);
return hasUi && hasAction;
}
@@ -4632,7 +4629,7 @@
return 0;
}
- protected final void showMagnifier() {
+ protected final void showMagnifier(@NonNull final MotionEvent event) {
if (mMagnifier == null) {
return;
}
@@ -4658,9 +4655,10 @@
final Layout layout = mTextView.getLayout();
final int lineNumber = layout.getLineForOffset(offset);
- // Horizontally snap to character offset.
- final float xPosInView = getHorizontal(mTextView.getLayout(), offset)
- + mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
+ // Horizontally move the magnifier smoothly.
+ final int[] textViewLocationOnScreen = new int[2];
+ mTextView.getLocationOnScreen(textViewLocationOnScreen);
+ final float xPosInView = event.getRawX() - textViewLocationOnScreen[0];
// Vertically snap to middle of current line.
final float yPosInView = (mTextView.getLayout().getLineTop(lineNumber)
+ mTextView.getLayout().getLineBottom(lineNumber)) / 2.0f
@@ -4855,11 +4853,11 @@
case MotionEvent.ACTION_DOWN:
mDownPositionX = ev.getRawX();
mDownPositionY = ev.getRawY();
- showMagnifier();
+ showMagnifier(ev);
break;
case MotionEvent.ACTION_MOVE:
- showMagnifier();
+ showMagnifier(ev);
break;
case MotionEvent.ACTION_UP:
@@ -5213,11 +5211,11 @@
// re-engages the handle.
mTouchWordDelta = 0.0f;
mPrevX = UNSET_X_VALUE;
- showMagnifier();
+ showMagnifier(event);
break;
case MotionEvent.ACTION_MOVE:
- showMagnifier();
+ showMagnifier(event);
break;
case MotionEvent.ACTION_UP:
diff --git a/android/widget/Magnifier.java b/android/widget/Magnifier.java
index 26dfcc2..310b170 100644
--- a/android/widget/Magnifier.java
+++ b/android/widget/Magnifier.java
@@ -32,6 +32,7 @@
import android.view.Surface;
import android.view.SurfaceView;
import android.view.View;
+import android.view.ViewParent;
import com.android.internal.util.Preconditions;
@@ -44,6 +45,8 @@
private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1;
// The view to which this magnifier is attached.
private final View mView;
+ // The coordinates of the view in the surface.
+ private final int[] mViewCoordinatesInSurface;
// The window containing the magnifier.
private final PopupWindow mWindow;
// The center coordinates of the window containing the magnifier.
@@ -87,6 +90,8 @@
com.android.internal.R.dimen.magnifier_height);
mZoomScale = context.getResources().getFloat(
com.android.internal.R.dimen.magnifier_zoom_scale);
+ // The view's surface coordinates will not be updated until the magnifier is first shown.
+ mViewCoordinatesInSurface = new int[2];
mWindow = new PopupWindow(context);
mWindow.setContentView(content);
@@ -120,9 +125,34 @@
configureCoordinates(xPosInView, yPosInView);
// Clamp startX value to avoid distorting the rendering of the magnifier content.
- final int startX = Math.max(0, Math.min(
+ // For this, we compute:
+ // - zeroScrollXInSurface: this is the start x of mView, where this is not masked by a
+ // potential scrolling container. For example, if mView is a
+ // TextView contained in a HorizontalScrollView,
+ // mViewCoordinatesInSurface will reflect the surface position of
+ // the first text character, rather than the position of the first
+ // visible one. Therefore, we need to add back the amount of
+ // scrolling from the parent containers.
+ // - actualWidth: similarly, the width of a View will be larger than its actually visible
+ // width when it is contained in a scrolling container. We need to use
+ // the minimum width of a scrolling container which contains this view.
+ int zeroScrollXInSurface = mViewCoordinatesInSurface[0];
+ int actualWidth = mView.getWidth();
+ ViewParent viewParent = mView.getParent();
+ while (viewParent instanceof View) {
+ final View container = (View) viewParent;
+ if (container.canScrollHorizontally(-1 /* left scroll */)
+ || container.canScrollHorizontally(1 /* right scroll */)) {
+ zeroScrollXInSurface += container.getScrollX();
+ actualWidth = Math.min(actualWidth, container.getWidth()
+ - container.getPaddingLeft() - container.getPaddingRight());
+ }
+ viewParent = viewParent.getParent();
+ }
+
+ final int startX = Math.max(zeroScrollXInSurface, Math.min(
mCenterZoomCoords.x - mBitmap.getWidth() / 2,
- mView.getWidth() - mBitmap.getWidth()));
+ zeroScrollXInSurface + actualWidth - mBitmap.getWidth()));
final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2;
if (xPosInView != mPrevPosInView.x || yPosInView != mPrevPosInView.y) {
@@ -169,10 +199,9 @@
posX = xPosInView;
posY = yPosInView;
} else {
- final int[] coordinatesInSurface = new int[2];
- mView.getLocationInSurface(coordinatesInSurface);
- posX = xPosInView + coordinatesInSurface[0];
- posY = yPosInView + coordinatesInSurface[1];
+ mView.getLocationInSurface(mViewCoordinatesInSurface);
+ posX = xPosInView + mViewCoordinatesInSurface[0];
+ posY = yPosInView + mViewCoordinatesInSurface[1];
}
mCenterZoomCoords.x = Math.round(posX);
diff --git a/android/widget/MediaControlView2.java b/android/widget/MediaControlView2.java
new file mode 100644
index 0000000..f1d633a
--- /dev/null
+++ b/android/widget/MediaControlView2.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2017 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.widget;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.session.MediaController;
+import android.media.update.ApiLoader;
+import android.media.update.MediaControlView2Provider;
+import android.media.update.ViewProvider;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+/**
+ * A View that contains the controls for MediaPlayer2.
+ * It provides a wide range of UI including buttons such as "Play/Pause", "Rewind", "Fast Forward",
+ * "Subtitle", "Full Screen", and it is also possible to add multiple custom buttons.
+ *
+ * <p>
+ * <em> MediaControlView2 can be initialized in two different ways: </em>
+ * 1) When VideoView2 is initialized, it automatically initializes a MediaControlView2 instance and
+ * adds it to the view.
+ * 2) Initialize MediaControlView2 programmatically and add it to a ViewGroup instance.
+ *
+ * In the first option, VideoView2 automatically connects MediaControlView2 to MediaController2,
+ * which is necessary to communicate with MediaSession2. In the second option, however, the
+ * developer needs to manually retrieve a MediaController2 instance and set it to MediaControlView2
+ * by calling setController(MediaController2 controller).
+ *
+ * TODO PUBLIC API
+ * @hide
+ */
+public class MediaControlView2 extends FrameLayout {
+ /** @hide */
+ @IntDef({
+ BUTTON_PLAY_PAUSE,
+ BUTTON_FFWD,
+ BUTTON_REW,
+ BUTTON_NEXT,
+ BUTTON_PREV,
+ BUTTON_SUBTITLE,
+ BUTTON_FULL_SCREEN,
+ BUTTON_OVERFLOW,
+ BUTTON_MUTE,
+ BUTTON_ASPECT_RATIO,
+ BUTTON_SETTINGS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Button {}
+
+ public static final int BUTTON_PLAY_PAUSE = 1;
+ public static final int BUTTON_FFWD = 2;
+ public static final int BUTTON_REW = 3;
+ public static final int BUTTON_NEXT = 4;
+ public static final int BUTTON_PREV = 5;
+ public static final int BUTTON_SUBTITLE = 6;
+ public static final int BUTTON_FULL_SCREEN = 7;
+ public static final int BUTTON_OVERFLOW = 8;
+ public static final int BUTTON_MUTE = 9;
+ public static final int BUTTON_ASPECT_RATIO = 10;
+ public static final int BUTTON_SETTINGS = 11;
+
+ private final MediaControlView2Provider mProvider;
+
+ public MediaControlView2(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ mProvider = ApiLoader.getProvider(context)
+ .createMediaControlView2(this, new SuperProvider());
+ }
+
+ /**
+ * @hide
+ */
+ public MediaControlView2Provider getProvider() {
+ return mProvider;
+ }
+
+ /**
+ * Sets MediaController2 instance to control corresponding MediaSession2.
+ */
+ public void setController(MediaController controller) {
+ mProvider.setController_impl(controller);
+ }
+
+ /**
+ * Shows the control view on screen. It will disappear automatically after 3 seconds of
+ * inactivity.
+ */
+ public void show() {
+ mProvider.show_impl();
+ }
+
+ /**
+ * Shows the control view on screen. It will disappear automatically after {@code timeout}
+ * milliseconds of inactivity.
+ */
+ public void show(int timeout) {
+ mProvider.show_impl(timeout);
+ }
+
+ /**
+ * Returns whether the control view is currently shown or hidden.
+ */
+ public boolean isShowing() {
+ return mProvider.isShowing_impl();
+ }
+
+ /**
+ * Hide the control view from the screen.
+ */
+ public void hide() {
+ mProvider.hide_impl();
+ }
+
+ /**
+ * If the media selected has a subtitle track, calling this method will display the subtitle at
+ * the bottom of the view. If a media has multiple subtitle tracks, this method will select the
+ * first one of them.
+ */
+ public void showSubtitle() {
+ mProvider.showSubtitle_impl();
+ }
+
+ /**
+ * Hides the currently displayed subtitle.
+ */
+ public void hideSubtitle() {
+ mProvider.hideSubtitle_impl();
+ }
+
+ /**
+ * Set listeners for previous and next buttons to customize the behavior of clicking them.
+ * The UI for these buttons are provided as default and will be automatically displayed when
+ * this method is called.
+ *
+ * @param next Listener for clicking next button
+ * @param prev Listener for clicking previous button
+ */
+ public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) {
+ mProvider.setPrevNextListeners_impl(next, prev);
+ }
+
+ /**
+ * Hides the specified button from view.
+ *
+ * @param button the constant integer assigned to individual buttons
+ * @param visible whether the button should be visible or not
+ */
+ public void setButtonVisibility(int button, boolean visible) {
+ mProvider.setButtonVisibility_impl(button, visible);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ mProvider.onAttachedToWindow_impl();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ mProvider.onDetachedFromWindow_impl();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return mProvider.getAccessibilityClassName_impl();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ return mProvider.onTouchEvent_impl(ev);
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent ev) {
+ return mProvider.onTrackballEvent_impl(ev);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return mProvider.onKeyDown_impl(keyCode, event);
+ }
+
+ @Override
+ public void onFinishInflate() {
+ mProvider.onFinishInflate_impl();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ return mProvider.dispatchKeyEvent_impl(event);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ mProvider.setEnabled_impl(enabled);
+ }
+
+ private class SuperProvider implements ViewProvider {
+ @Override
+ public void onAttachedToWindow_impl() {
+ MediaControlView2.super.onAttachedToWindow();
+ }
+
+ @Override
+ public void onDetachedFromWindow_impl() {
+ MediaControlView2.super.onDetachedFromWindow();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName_impl() {
+ return MediaControlView2.super.getAccessibilityClassName();
+ }
+
+ @Override
+ public boolean onTouchEvent_impl(MotionEvent ev) {
+ return MediaControlView2.super.onTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onTrackballEvent_impl(MotionEvent ev) {
+ return MediaControlView2.super.onTrackballEvent(ev);
+ }
+
+ @Override
+ public boolean onKeyDown_impl(int keyCode, KeyEvent event) {
+ return MediaControlView2.super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public void onFinishInflate_impl() {
+ MediaControlView2.super.onFinishInflate();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent_impl(KeyEvent event) {
+ return MediaControlView2.super.dispatchKeyEvent(event);
+ }
+
+ @Override
+ public void setEnabled_impl(boolean enabled) {
+ MediaControlView2.super.setEnabled(enabled);
+ }
+ }
+}
diff --git a/android/widget/SelectionActionModeHelper.java b/android/widget/SelectionActionModeHelper.java
index 2c6466c..3bfa520 100644
--- a/android/widget/SelectionActionModeHelper.java
+++ b/android/widget/SelectionActionModeHelper.java
@@ -235,10 +235,13 @@
@Editor.TextActionMode int actionMode, @Nullable SelectionResult result) {
final CharSequence text = getText(mTextView);
if (result != null && text instanceof Spannable
- && (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
+ && (mTextView.isTextSelectable()
+ || mTextView.isTextEditable()
+ || actionMode == Editor.TextActionMode.TEXT_LINK)) {
// Do not change the selection if TextClassifier should be dark launched.
if (!mTextView.getTextClassifier().getSettings().isDarkLaunch()) {
Selection.setSelection((Spannable) text, result.mStart, result.mEnd);
+ mTextView.invalidate();
}
mTextClassification = result.mClassification;
} else {
@@ -250,8 +253,17 @@
&& (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
controller.show();
}
- if (result != null && actionMode == Editor.TextActionMode.SELECTION) {
- mSelectionTracker.onSmartSelection(result);
+ if (result != null) {
+ switch (actionMode) {
+ case Editor.TextActionMode.SELECTION:
+ mSelectionTracker.onSmartSelection(result);
+ break;
+ case Editor.TextActionMode.TEXT_LINK:
+ mSelectionTracker.onLinkSelected(result);
+ break;
+ default:
+ break;
+ }
}
}
mEditor.setRestartActionModeOnNextRefresh(false);
@@ -486,12 +498,24 @@
* Called when selection action mode is started and the results come from a classifier.
*/
public void onSmartSelection(SelectionResult result) {
+ onClassifiedSelection(result);
+ mLogger.logSelectionModified(
+ result.mStart, result.mEnd, result.mClassification, result.mSelection);
+ }
+
+ /**
+ * Called when link action mode is started and the classification comes from a classifier.
+ */
+ public void onLinkSelected(SelectionResult result) {
+ onClassifiedSelection(result);
+ // TODO: log (b/70246800)
+ }
+
+ private void onClassifiedSelection(SelectionResult result) {
if (isSelectionStarted()) {
mSelectionStart = result.mStart;
mSelectionEnd = result.mEnd;
mAllowReset = mSelectionStart != mOriginalStart || mSelectionEnd != mOriginalEnd;
- mLogger.logSelectionModified(
- result.mStart, result.mEnd, result.mClassification, result.mSelection);
}
}
diff --git a/android/widget/TextView.java b/android/widget/TextView.java
index 1e17f34..7d3fcf4 100644
--- a/android/widget/TextView.java
+++ b/android/widget/TextView.java
@@ -27,8 +27,10 @@
import android.annotation.DrawableRes;
import android.annotation.FloatRange;
import android.annotation.IntDef;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.Px;
import android.annotation.Size;
import android.annotation.StringRes;
import android.annotation.StyleRes;
@@ -44,6 +46,7 @@
import android.content.res.ColorStateList;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
+import android.content.res.ResourceId;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
@@ -51,6 +54,7 @@
import android.graphics.Canvas;
import android.graphics.Insets;
import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.Rect;
@@ -76,8 +80,8 @@
import android.text.InputFilter;
import android.text.InputType;
import android.text.Layout;
+import android.text.MeasuredText;
import android.text.ParcelableSpan;
-import android.text.PremeasuredText;
import android.text.Selection;
import android.text.SpanWatcher;
import android.text.Spannable;
@@ -295,6 +299,7 @@
* @attr ref android.R.styleable#TextView_imeActionId
* @attr ref android.R.styleable#TextView_editorExtras
* @attr ref android.R.styleable#TextView_elegantTextHeight
+ * @attr ref android.R.styleable#TextView_fallbackLineSpacing
* @attr ref android.R.styleable#TextView_letterSpacing
* @attr ref android.R.styleable#TextView_fontFeatureSettings
* @attr ref android.R.styleable#TextView_breakStrategy
@@ -304,12 +309,12 @@
* @attr ref android.R.styleable#TextView_autoSizeMaxTextSize
* @attr ref android.R.styleable#TextView_autoSizeStepGranularity
* @attr ref android.R.styleable#TextView_autoSizePresetSizes
+ * @attr ref android.R.styleable#TextView_accessibilityHeading
*/
@RemoteView
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
static final String LOG_TAG = "TextView";
static final boolean DEBUG_EXTRACT = false;
- static final boolean DEBUG_AUTOFILL = false;
private static final float[] TEMP_POSITION = new float[2];
// Enum for the "typeface" XML parameter.
@@ -399,6 +404,7 @@
private int mCurTextColor;
private int mCurHintTextColor;
private boolean mFreezesText;
+ private boolean mIsAccessibilityHeading;
private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
@@ -654,7 +660,7 @@
// True if internationalized input should be used for numbers and date and time.
private final boolean mUseInternationalizedInput;
// True if fallback fonts that end up getting used should be allowed to affect line spacing.
- /* package */ final boolean mUseFallbackLineSpacing;
+ /* package */ boolean mUseFallbackLineSpacing;
@ViewDebug.ExportedProperty(category = "text")
private int mGravity = Gravity.TOP | Gravity.START;
@@ -785,9 +791,11 @@
// mAutoSizeStepGranularityInPx.
private boolean mHasPresetAutoSizeValues = false;
- // Indicates whether the text was set from resources or dynamically, so it can be used to
+ // Indicates whether the text was set statically or dynamically, so it can be used to
// sanitize autofill requests.
- private boolean mTextFromResource = false;
+ private boolean mTextSetFromXmlOrResourceId = false;
+ // Resource id used to set the text - used for autofill purposes.
+ private @StringRes int mTextId = ResourceId.ID_NULL;
/**
* Kick-start the font cache for the zygote process (to pay the cost of
@@ -921,12 +929,16 @@
int inputType = EditorInfo.TYPE_NULL;
a = theme.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);
+ int firstBaselineToTopHeight = -1;
+ int lastBaselineToBottomHeight = -1;
+ int lineHeight = -1;
readTextAppearance(context, a, attributes, true /* styleArray */);
int n = a.getIndexCount();
- boolean fromResourceId = false;
+ // Must set id in a temporary variable because it will be reset by setText()
+ boolean textIsSetFromXml = false;
for (int i = 0; i < n; i++) {
int attr = a.getIndex(i);
@@ -1068,7 +1080,8 @@
break;
case com.android.internal.R.styleable.TextView_text:
- fromResourceId = true;
+ textIsSetFromXml = true;
+ mTextId = a.getResourceId(attr, ResourceId.ID_NULL);
text = a.getText(attr);
break;
@@ -1244,6 +1257,20 @@
case com.android.internal.R.styleable.TextView_justificationMode:
mJustificationMode = a.getInt(attr, Layout.JUSTIFICATION_MODE_NONE);
break;
+
+ case com.android.internal.R.styleable.TextView_firstBaselineToTopHeight:
+ firstBaselineToTopHeight = a.getDimensionPixelSize(attr, -1);
+ break;
+
+ case com.android.internal.R.styleable.TextView_lastBaselineToBottomHeight:
+ lastBaselineToBottomHeight = a.getDimensionPixelSize(attr, -1);
+ break;
+
+ case com.android.internal.R.styleable.TextView_lineHeight:
+ lineHeight = a.getDimensionPixelSize(attr, -1);
+ break;
+ case com.android.internal.R.styleable.TextView_accessibilityHeading:
+ mIsAccessibilityHeading = a.getBoolean(attr, false);
}
}
@@ -1460,8 +1487,8 @@
}
setText(text, bufferType);
- if (fromResourceId) {
- mTextFromResource = true;
+ if (textIsSetFromXml) {
+ mTextSetFromXmlOrResourceId = true;
}
if (hint != null) setHint(hint);
@@ -1558,6 +1585,16 @@
} else {
mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_NONE;
}
+
+ if (firstBaselineToTopHeight >= 0) {
+ setFirstBaselineToTopHeight(firstBaselineToTopHeight);
+ }
+ if (lastBaselineToBottomHeight >= 0) {
+ setLastBaselineToBottomHeight(lastBaselineToBottomHeight);
+ }
+ if (lineHeight >= 0) {
+ setLineHeight(lineHeight);
+ }
}
/**
@@ -2360,7 +2397,7 @@
setText(mText);
if (hasPasswordTransformationMethod()) {
- notifyViewAccessibilityStateChangedIfNeeded(
+ notifyAccessibilityStateChanged(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
}
@@ -3160,6 +3197,12 @@
}
}
+ /**
+ * @inheritDoc
+ *
+ * @see #setFirstBaselineToTopHeight(int)
+ * @see #setLastBaselineToBottomHeight(int)
+ */
@Override
public void setPadding(int left, int top, int right, int bottom) {
if (left != mPaddingLeft
@@ -3174,6 +3217,12 @@
invalidate();
}
+ /**
+ * @inheritDoc
+ *
+ * @see #setFirstBaselineToTopHeight(int)
+ * @see #setLastBaselineToBottomHeight(int)
+ */
@Override
public void setPaddingRelative(int start, int top, int end, int bottom) {
if (start != getPaddingStart()
@@ -3189,6 +3238,97 @@
}
/**
+ * Updates the top padding of the TextView so that {@code firstBaselineToTopHeight} is
+ * equal to the distance between the firt text baseline and the top of this TextView.
+ * <strong>Note</strong> that if {@code FontMetrics.top} or {@code FontMetrics.ascent} was
+ * already greater than {@code firstBaselineToTopHeight}, the top padding is not updated.
+ *
+ * @param firstBaselineToTopHeight distance between first baseline to top of the container
+ * in pixels
+ *
+ * @see #getFirstBaselineToTopHeight()
+ * @see #setPadding(int, int, int, int)
+ * @see #setPaddingRelative(int, int, int, int)
+ *
+ * @attr ref android.R.styleable#TextView_firstBaselineToTopHeight
+ */
+ public void setFirstBaselineToTopHeight(@Px @IntRange(from = 0) int firstBaselineToTopHeight) {
+ Preconditions.checkArgumentNonnegative(firstBaselineToTopHeight);
+
+ final FontMetricsInt fontMetrics = getPaint().getFontMetricsInt();
+ final int fontMetricsTop;
+ if (getIncludeFontPadding()) {
+ fontMetricsTop = fontMetrics.top;
+ } else {
+ fontMetricsTop = fontMetrics.ascent;
+ }
+
+ // TODO: Decide if we want to ignore density ratio (i.e. when the user changes font size
+ // in settings). At the moment, we don't.
+
+ if (firstBaselineToTopHeight > Math.abs(fontMetricsTop)) {
+ final int paddingTop = firstBaselineToTopHeight - (-fontMetricsTop);
+ setPadding(getPaddingLeft(), paddingTop, getPaddingRight(), getPaddingBottom());
+ }
+ }
+
+ /**
+ * Updates the bottom padding of the TextView so that {@code lastBaselineToBottomHeight} is
+ * equal to the distance between the last text baseline and the bottom of this TextView.
+ * <strong>Note</strong> that if {@code FontMetrics.bottom} or {@code FontMetrics.descent} was
+ * already greater than {@code lastBaselineToBottomHeight}, the bottom padding is not updated.
+ *
+ * @param lastBaselineToBottomHeight distance between last baseline to bottom of the container
+ * in pixels
+ *
+ * @see #getLastBaselineToBottomHeight()
+ * @see #setPadding(int, int, int, int)
+ * @see #setPaddingRelative(int, int, int, int)
+ *
+ * @attr ref android.R.styleable#TextView_lastBaselineToBottomHeight
+ */
+ public void setLastBaselineToBottomHeight(
+ @Px @IntRange(from = 0) int lastBaselineToBottomHeight) {
+ Preconditions.checkArgumentNonnegative(lastBaselineToBottomHeight);
+
+ final FontMetricsInt fontMetrics = getPaint().getFontMetricsInt();
+ final int fontMetricsBottom;
+ if (getIncludeFontPadding()) {
+ fontMetricsBottom = fontMetrics.bottom;
+ } else {
+ fontMetricsBottom = fontMetrics.descent;
+ }
+
+ // TODO: Decide if we want to ignore density ratio (i.e. when the user changes font size
+ // in settings). At the moment, we don't.
+
+ if (lastBaselineToBottomHeight > Math.abs(fontMetricsBottom)) {
+ final int paddingBottom = lastBaselineToBottomHeight - fontMetricsBottom;
+ setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), paddingBottom);
+ }
+ }
+
+ /**
+ * Returns the distance between the first text baseline and the top of this TextView.
+ *
+ * @see #setFirstBaselineToTopHeight(int)
+ * @attr ref android.R.styleable#TextView_firstBaselineToTopHeight
+ */
+ public int getFirstBaselineToTopHeight() {
+ return getPaddingTop() - getPaint().getFontMetricsInt().top;
+ }
+
+ /**
+ * Returns the distance between the last text baseline and the bottom of this TextView.
+ *
+ * @see #setLastBaselineToBottomHeight(int)
+ * @attr ref android.R.styleable#TextView_lastBaselineToBottomHeight
+ */
+ public int getLastBaselineToBottomHeight() {
+ return getPaddingBottom() + getPaint().getFontMetricsInt().bottom;
+ }
+
+ /**
* Gets the autolink mask of the text. See {@link
* android.text.util.Linkify#ALL Linkify.ALL} and peers for
* possible values.
@@ -3250,6 +3390,8 @@
float mShadowDx = 0, mShadowDy = 0, mShadowRadius = 0;
boolean mHasElegant = false;
boolean mElegant = false;
+ boolean mHasFallbackLineSpacing = false;
+ boolean mFallbackLineSpacing = false;
boolean mHasLetterSpacing = false;
float mLetterSpacing = 0;
String mFontFeatureSettings = null;
@@ -3274,6 +3416,8 @@
+ " mShadowRadius:" + mShadowRadius + "\n"
+ " mHasElegant:" + mHasElegant + "\n"
+ " mElegant:" + mElegant + "\n"
+ + " mHasFallbackLineSpacing:" + mHasFallbackLineSpacing + "\n"
+ + " mFallbackLineSpacing:" + mFallbackLineSpacing + "\n"
+ " mHasLetterSpacing:" + mHasLetterSpacing + "\n"
+ " mLetterSpacing:" + mLetterSpacing + "\n"
+ " mFontFeatureSettings:" + mFontFeatureSettings + "\n"
@@ -3312,6 +3456,8 @@
com.android.internal.R.styleable.TextAppearance_shadowRadius);
sAppearanceValues.put(com.android.internal.R.styleable.TextView_elegantTextHeight,
com.android.internal.R.styleable.TextAppearance_elegantTextHeight);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_fallbackLineSpacing,
+ com.android.internal.R.styleable.TextAppearance_fallbackLineSpacing);
sAppearanceValues.put(com.android.internal.R.styleable.TextView_letterSpacing,
com.android.internal.R.styleable.TextAppearance_letterSpacing);
sAppearanceValues.put(com.android.internal.R.styleable.TextView_fontFeatureSettings,
@@ -3402,6 +3548,11 @@
attributes.mHasElegant = true;
attributes.mElegant = appearance.getBoolean(attr, attributes.mElegant);
break;
+ case com.android.internal.R.styleable.TextAppearance_fallbackLineSpacing:
+ attributes.mHasFallbackLineSpacing = true;
+ attributes.mFallbackLineSpacing = appearance.getBoolean(attr,
+ attributes.mFallbackLineSpacing);
+ break;
case com.android.internal.R.styleable.TextAppearance_letterSpacing:
attributes.mHasLetterSpacing = true;
attributes.mLetterSpacing =
@@ -3455,6 +3606,10 @@
setElegantTextHeight(attributes.mElegant);
}
+ if (attributes.mHasFallbackLineSpacing) {
+ setFallbackLineSpacing(attributes.mFallbackLineSpacing);
+ }
+
if (attributes.mHasLetterSpacing) {
setLetterSpacing(attributes.mLetterSpacing);
}
@@ -3736,7 +3891,8 @@
*
* @param elegant set the paint's elegant metrics flag.
*
- * @see Paint#isElegantTextHeight(boolean)
+ * @see #isElegantTextHeight()
+ * @see Paint#isElegantTextHeight()
*
* @attr ref android.R.styleable#TextView_elegantTextHeight
*/
@@ -3752,6 +3908,43 @@
}
/**
+ * Set whether to respect the ascent and descent of the fallback fonts that are used in
+ * displaying the text (which is needed to avoid text from consecutive lines running into
+ * each other). If set, fallback fonts that end up getting used can increase the ascent
+ * and descent of the lines that they are used on.
+ * <p/>
+ * It is required to be true if text could be in languages like Burmese or Tibetan where text
+ * is typically much taller or deeper than Latin text.
+ *
+ * @param enabled whether to expand linespacing based on fallback fonts, {@code true} by default
+ *
+ * @see StaticLayout.Builder#setUseLineSpacingFromFallbacks(boolean)
+ *
+ * @attr ref android.R.styleable#TextView_fallbackLineSpacing
+ */
+ public void setFallbackLineSpacing(boolean enabled) {
+ if (mUseFallbackLineSpacing != enabled) {
+ mUseFallbackLineSpacing = enabled;
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * @return whether fallback line spacing is enabled, {@code true} by default
+ *
+ * @see #setFallbackLineSpacing(boolean)
+ *
+ * @attr ref android.R.styleable#TextView_fallbackLineSpacing
+ */
+ public boolean isFallbackLineSpacing() {
+ return mUseFallbackLineSpacing;
+ }
+
+ /**
* Get the value of the TextView's elegant height metrics flag. This setting selects font
* variants that have not been compacted to fit Latin-based vertical
* metrics, and also increases top and bottom bounds to provide more space.
@@ -4917,6 +5110,53 @@
}
/**
+ * Sets an explicit line height for this TextView. This is equivalent to the vertical distance
+ * between subsequent baselines in the TextView.
+ *
+ * @param lineHeight the line height in pixels
+ *
+ * @see #setLineSpacing(float, float)
+ * @see #getLineSpacing()
+ *
+ * @attr ref android.R.styleable#TextView_lineHeight
+ */
+ public void setLineHeight(@Px @IntRange(from = 0) int lineHeight) {
+ Preconditions.checkArgumentNonnegative(lineHeight);
+
+ final int fontHeight = getPaint().getFontMetricsInt(null);
+ // Make sure we don't setLineSpacing if it's not needed to avoid unnecessary redraw.
+ if (lineHeight != fontHeight) {
+ // Set lineSpacingExtra by the difference of lineSpacing with lineHeight
+ setLineSpacing(lineHeight - fontHeight, 1f);
+ }
+ }
+
+ /**
+ * Gets whether this view is a heading for accessibility purposes.
+ *
+ * @return {@code true} if the view is a heading, {@code false} otherwise.
+ *
+ * @attr ref android.R.styleable#TextView_accessibilityHeading
+ */
+ public boolean isAccessibilityHeading() {
+ return mIsAccessibilityHeading;
+ }
+
+ /**
+ * Set if view is a heading for a section of content for accessibility purposes.
+ *
+ * @param isHeading {@code true} if the view is a heading, {@code false} otherwise.
+ *
+ * @attr ref android.R.styleable#TextView_accessibilityHeading
+ */
+ public void setAccessibilityHeading(boolean isHeading) {
+ if (isHeading != mIsAccessibilityHeading) {
+ mIsAccessibilityHeading = isHeading;
+ notifyAccessibilityStateChanged(AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+ }
+
+ /**
* Convenience method to append the specified text to the TextView's
* display buffer, upgrading it to {@link android.widget.TextView.BufferType#EDITABLE}
* if it was not already editable.
@@ -5278,7 +5518,7 @@
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
- mTextFromResource = false;
+ mTextSetFromXmlOrResourceId = false;
if (text == null) {
text = "";
}
@@ -5336,7 +5576,7 @@
if (imm != null) imm.restartInput(this);
} else if (type == BufferType.SPANNABLE || mMovement != null) {
text = mSpannableFactory.newSpannable(text);
- } else if (!(text instanceof PremeasuredText || text instanceof CharWrapper)) {
+ } else if (!(text instanceof MeasuredText || text instanceof CharWrapper)) {
text = TextUtils.stringOrSpannedString(text);
}
@@ -5419,7 +5659,7 @@
sendOnTextChanged(text, 0, oldlen, textLength);
onTextChanged(text, 0, oldlen, textLength);
- notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
+ notifyAccessibilityStateChanged(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
if (needEditableForNotification) {
sendAfterTextChanged((Editable) text);
@@ -5516,7 +5756,8 @@
@android.view.RemotableViewMethod
public final void setText(@StringRes int resid) {
setText(getContext().getResources().getText(resid));
- mTextFromResource = true;
+ mTextSetFromXmlOrResourceId = true;
+ mTextId = resid;
}
/**
@@ -5543,7 +5784,8 @@
*/
public final void setText(@StringRes int resid, BufferType type) {
setText(getContext().getResources().getText(resid), type);
- mTextFromResource = true;
+ mTextSetFromXmlOrResourceId = true;
+ mTextId = resid;
}
/**
@@ -6151,7 +6393,7 @@
public void setError(CharSequence error, Drawable icon) {
createEditorIfNeeded();
mEditor.setError(error, icon);
- notifyViewAccessibilityStateChangedIfNeeded(
+ notifyAccessibilityStateChanged(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
}
@@ -9066,8 +9308,7 @@
/**
*
- * Checks whether the transformation method applied to this TextView is set to ALL CAPS. This
- * settings is internally ignored if this field is editable or selectable.
+ * Checks whether the transformation method applied to this TextView is set to ALL CAPS.
* @return Whether the current transformation method is for ALL CAPS.
*
* @see #setAllCaps(boolean)
@@ -9456,7 +9697,7 @@
}
final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
if (afm != null) {
- if (DEBUG_AUTOFILL) {
+ if (android.view.autofill.Helper.sVerbose) {
Log.v(LOG_TAG, "sendAfterTextChanged(): notify AFM for text=" + mText);
}
afm.notifyValueChanged(TextView.this);
@@ -10234,7 +10475,17 @@
final boolean isPassword = hasPasswordTransformationMethod()
|| isPasswordInputType(getInputType());
if (forAutofill) {
- structure.setDataIsSensitive(!mTextFromResource);
+ structure.setDataIsSensitive(!mTextSetFromXmlOrResourceId);
+ if (mTextId != ResourceId.ID_NULL) {
+ try {
+ structure.setTextIdEntry(getResources().getResourceEntryName(mTextId));
+ } catch (Resources.NotFoundException e) {
+ if (android.view.autofill.Helper.sVerbose) {
+ Log.v(LOG_TAG, "onProvideAutofillStructure(): cannot set name for text id "
+ + mTextId + ": " + e.getMessage());
+ }
+ }
+ }
}
if (!isPassword || forAutofill) {
@@ -10455,6 +10706,7 @@
info.setText(getTextForAccessibility());
info.setHintText(mHint);
info.setShowingHintText(isShowingHint());
+ info.setHeading(mIsAccessibilityHeading);
if (mBufferType == BufferType.EDITABLE) {
info.setEditable(true);
@@ -10942,6 +11194,12 @@
return true;
case ID_COPY:
+ // For link action mode in a non-selectable/non-focusable TextView,
+ // make sure that we set the appropriate min/max.
+ final int selStart = getSelectionStart();
+ final int selEnd = getSelectionEnd();
+ min = Math.max(0, Math.min(selStart, selEnd));
+ max = Math.max(0, Math.max(selStart, selEnd));
final ClipData copyData = ClipData.newPlainText(null, getTransformedText(min, max));
if (setPrimaryClip(copyData)) {
stopTextActionMode();
@@ -11164,11 +11422,9 @@
*/
public boolean requestActionMode(@NonNull TextLinks.TextLink link) {
Preconditions.checkNotNull(link);
- if (mEditor != null) {
- mEditor.startLinkActionModeAsync(link);
- return true;
- }
- return false;
+ createEditorIfNeeded();
+ mEditor.startLinkActionModeAsync(link);
+ return true;
}
/**
* @hide
@@ -11883,7 +12139,7 @@
private final Choreographer mChoreographer;
private byte mStatus = MARQUEE_STOPPED;
- private final float mPixelsPerSecond;
+ private final float mPixelsPerMs;
private float mMaxScroll;
private float mMaxFadeScroll;
private float mGhostStart;
@@ -11896,7 +12152,7 @@
Marquee(TextView v) {
final float density = v.getContext().getResources().getDisplayMetrics().density;
- mPixelsPerSecond = MARQUEE_DP_PER_SECOND * density;
+ mPixelsPerMs = MARQUEE_DP_PER_SECOND * density / 1000f;
mView = new WeakReference<TextView>(v);
mChoreographer = Choreographer.getInstance();
}
@@ -11941,7 +12197,7 @@
long currentMs = mChoreographer.getFrameTime();
long deltaMs = currentMs - mLastAnimationMs;
mLastAnimationMs = currentMs;
- float deltaPx = deltaMs / 1000f * mPixelsPerSecond;
+ float deltaPx = deltaMs * mPixelsPerMs;
mScroll += deltaPx;
if (mScroll > mMaxScroll) {
mScroll = mMaxScroll;
diff --git a/android/widget/VideoView2.java b/android/widget/VideoView2.java
new file mode 100644
index 0000000..8650c0a
--- /dev/null
+++ b/android/widget/VideoView2.java
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2018 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.widget;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.MediaPlayerBase;
+import android.media.update.ApiLoader;
+import android.media.update.VideoView2Provider;
+import android.media.update.ViewProvider;
+import android.net.Uri;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Map;
+
+// TODO: Use @link tag to refer MediaPlayer2 in docs once MediaPlayer2.java is submitted. Same to
+// MediaSession2.
+// TODO: change the reference from MediaPlayer to MediaPlayer2.
+/**
+ * Displays a video file. VideoView2 class is a View class which is wrapping MediaPlayer2 so that
+ * developers can easily implement a video rendering application.
+ *
+ * <p>
+ * <em> Data sources that VideoView2 supports : </em>
+ * VideoView2 can play video files and audio-only fiels as
+ * well. It can load from various sources such as resources or content providers. The supported
+ * media file formats are the same as MediaPlayer2.
+ *
+ * <p>
+ * <em> View type can be selected : </em>
+ * VideoView2 can render videos on top of TextureView as well as
+ * SurfaceView selectively. The default is SurfaceView and it can be changed using
+ * {@link #setViewType(int)} method. Using SurfaceView is recommended in most cases for saving
+ * battery. TextureView might be preferred for supporting various UIs such as animation and
+ * translucency.
+ *
+ * <p>
+ * <em> Differences between {@link VideoView} class : </em>
+ * VideoView2 covers and inherits the most of
+ * VideoView's functionalities. The main differences are
+ * <ul>
+ * <li> VideoView2 inherits FrameLayout and renders videos using SurfaceView and TextureView
+ * selectively while VideoView inherits SurfaceView class.
+ * <li> VideoView2 is integrated with MediaControlView2 and a default MediaControlView2 instance is
+ * attached to VideoView2 by default. If a developer does not want to use the default
+ * MediaControlView2, needs to set enableControlView attribute to false. For instance,
+ * <pre>
+ * <VideoView2
+ * android:id="@+id/video_view"
+ * xmlns:widget="http://schemas.android.com/apk/com.android.media.update"
+ * widget:enableControlView="false" />
+ * </pre>
+ * If a developer wants to attach a customed MediaControlView2, then set enableControlView attribute
+ * to false and assign the customed media control widget using {@link #setMediaControlView2}.
+ * <li> VideoView2 is integrated with MediaPlayer2 while VideoView is integrated with MediaPlayer.
+ * <li> VideoView2 is integrated with MediaSession2 and so it responses with media key events.
+ * A VideoView2 keeps a MediaSession2 instance internally and connects it to a corresponding
+ * MediaControlView2 instance.
+ * </p>
+ * </ul>
+ *
+ * <p>
+ * <em> Audio focus and audio attributes : </em>
+ * By default, VideoView2 requests audio focus with
+ * {@link AudioManager#AUDIOFOCUS_GAIN}. Use {@link #setAudioFocusRequest(int)} to change this
+ * behavior. The default {@link AudioAttributes} used during playback have a usage of
+ * {@link AudioAttributes#USAGE_MEDIA} and a content type of
+ * {@link AudioAttributes#CONTENT_TYPE_MOVIE}, use {@link #setAudioAttributes(AudioAttributes)} to
+ * modify them.
+ *
+ * <p>
+ * Note: VideoView2 does not retain its full state when going into the background. In particular, it
+ * does not restore the current play state, play position, selected tracks. Applications should save
+ * and restore these on their own in {@link android.app.Activity#onSaveInstanceState} and
+ * {@link android.app.Activity#onRestoreInstanceState}.
+ *
+ * @hide
+ */
+public class VideoView2 extends FrameLayout {
+ /** @hide */
+ @IntDef({
+ VIEW_TYPE_TEXTUREVIEW,
+ VIEW_TYPE_SURFACEVIEW
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ViewType {}
+
+ public static final int VIEW_TYPE_SURFACEVIEW = 1;
+ public static final int VIEW_TYPE_TEXTUREVIEW = 2;
+
+ private final VideoView2Provider mProvider;
+
+ public VideoView2(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public VideoView2(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public VideoView2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public VideoView2(
+ @NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ mProvider = ApiLoader.getProvider(context).createVideoView2(this, new SuperProvider(),
+ attrs, defStyleAttr, defStyleRes);
+ }
+
+ /**
+ * @hide
+ */
+ public VideoView2Provider getProvider() {
+ return mProvider;
+ }
+
+ /**
+ * Sets MediaControlView2 instance. It will replace the previously assigned MediaControlView2
+ * instance if any.
+ *
+ * @param mediaControlView a media control view2 instance.
+ */
+ public void setMediaControlView2(MediaControlView2 mediaControlView) {
+ mProvider.setMediaControlView2_impl(mediaControlView);
+ }
+
+ /**
+ * Returns MediaControlView2 instance which is currently attached to VideoView2 by default or by
+ * {@link #setMediaControlView2} method.
+ */
+ public MediaControlView2 getMediaControlView2() {
+ return mProvider.getMediaControlView2_impl();
+ }
+
+ /**
+ * Starts playback with the media contents specified by {@link #setVideoURI} and
+ * {@link #setVideoPath}.
+ * If it has been paused, this method will resume playback from the current position.
+ */
+ public void start() {
+ mProvider.start_impl();
+ }
+
+ /**
+ * Pauses playback.
+ */
+ public void pause() {
+ mProvider.pause_impl();
+ }
+
+ /**
+ * Gets the duration of the media content specified by #setVideoURI and #setVideoPath
+ * in milliseconds.
+ */
+ public int getDuration() {
+ return mProvider.getDuration_impl();
+ }
+
+ /**
+ * Gets current playback position in milliseconds.
+ */
+ public int getCurrentPosition() {
+ return mProvider.getCurrentPosition_impl();
+ }
+
+ // TODO: mention about key-frame related behavior.
+ /**
+ * Moves the media by specified time position.
+ * @param msec the offset in milliseconds from the start to seek to.
+ */
+ public void seekTo(int msec) {
+ mProvider.seekTo_impl(msec);
+ }
+
+ /**
+ * Says if the media is currently playing.
+ * @return true if the media is playing, false if it is not (eg. paused or stopped).
+ */
+ public boolean isPlaying() {
+ return mProvider.isPlaying_impl();
+ }
+
+ // TODO: check what will return if it is a local media.
+ /**
+ * Gets the percentage (0-100) of the content that has been buffered or played so far.
+ */
+ public int getBufferPercentage() {
+ return mProvider.getBufferPercentage_impl();
+ }
+
+ /**
+ * Returns the audio session ID.
+ */
+ public int getAudioSessionId() {
+ return mProvider.getAudioSessionId_impl();
+ }
+
+ /**
+ * Starts rendering closed caption or subtitles if there is any. The first subtitle track will
+ * be chosen by default if there multiple subtitle tracks exist.
+ */
+ public void showSubtitle() {
+ mProvider.showSubtitle_impl();
+ }
+
+ /**
+ * Stops showing closed captions or subtitles.
+ */
+ public void hideSubtitle() {
+ mProvider.hideSubtitle_impl();
+ }
+
+ /**
+ * Sets full screen mode.
+ */
+ public void setFullScreen(boolean fullScreen) {
+ mProvider.setFullScreen_impl(fullScreen);
+ }
+
+ // TODO: This should be revised after integration with MediaPlayer2.
+ /**
+ * Sets playback speed.
+ *
+ * It is expressed as a multiplicative factor, where normal speed is 1.0f. If it is less than
+ * or equal to zero, it will be just ignored and nothing will be changed. If it exceeds the
+ * maximum speed that internal engine supports, system will determine best handling or it will
+ * be reset to the normal speed 1.0f.
+ * @param speed the playback speed. It should be positive.
+ */
+ public void setSpeed(float speed) {
+ mProvider.setSpeed_impl(speed);
+ }
+
+ /**
+ * Returns current speed setting.
+ *
+ * If setSpeed() has never been called, returns the default value 1.0f.
+ * @return current speed setting
+ */
+ public float getSpeed() {
+ return mProvider.getSpeed_impl();
+ }
+
+ /**
+ * Sets which type of audio focus will be requested during the playback, or configures playback
+ * to not request audio focus. Valid values for focus requests are
+ * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use
+ * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be
+ * requested when playback starts. You can for instance use this when playing a silent animation
+ * through this class, and you don't want to affect other audio applications playing in the
+ * background.
+ *
+ * @param focusGain the type of audio focus gain that will be requested, or
+ * {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during
+ * playback.
+ */
+ public void setAudioFocusRequest(int focusGain) {
+ mProvider.setAudioFocusRequest_impl(focusGain);
+ }
+
+ /**
+ * Sets the {@link AudioAttributes} to be used during the playback of the video.
+ *
+ * @param attributes non-null <code>AudioAttributes</code>.
+ */
+ public void setAudioAttributes(@NonNull AudioAttributes attributes) {
+ mProvider.setAudioAttributes_impl(attributes);
+ }
+
+ /**
+ * Sets a remote player for handling playback of the selected route from MediaControlView2.
+ * If this is not called, MediaCotrolView2 will not show the route button.
+ *
+ * @param routeCategories the list of media control categories in
+ * {@link android.support.v7.media.MediaControlIntent}
+ * @param player the player to handle the selected route. If null, a default
+ * route player will be used.
+ * @throws IllegalStateException if MediaControlView2 is not set.
+ */
+ public void setRouteAttributes(@NonNull List<String> routeCategories,
+ @Nullable MediaPlayerBase player) {
+ mProvider.setRouteAttributes_impl(routeCategories, player);
+ }
+
+ /**
+ * Sets video path.
+ *
+ * @param path the path of the video.
+ */
+ public void setVideoPath(String path) {
+ mProvider.setVideoPath_impl(path);
+ }
+
+ /**
+ * Sets video URI.
+ *
+ * @param uri the URI of the video.
+ */
+ public void setVideoURI(Uri uri) {
+ mProvider.setVideoURI_impl(uri);
+ }
+
+ /**
+ * Sets video URI using specific headers.
+ *
+ * @param uri the URI of the video.
+ * @param headers the headers for the URI request.
+ * Note that the cross domain redirection is allowed by default, but that can be
+ * changed with key/value pairs through the headers parameter with
+ * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
+ * to disallow or allow cross domain redirection.
+ */
+ public void setVideoURI(Uri uri, Map<String, String> headers) {
+ mProvider.setVideoURI_impl(uri, headers);
+ }
+
+ /**
+ * Selects which view will be used to render video between SurfacView and TextureView.
+ *
+ * @param viewType the view type to render video
+ * <ul>
+ * <li>{@link #VIEW_TYPE_SURFACEVIEW}
+ * <li>{@link #VIEW_TYPE_TEXTUREVIEW}
+ * </ul>
+ */
+ public void setViewType(@ViewType int viewType) {
+ mProvider.setViewType_impl(viewType);
+ }
+
+ /**
+ * Returns view type.
+ *
+ * @return view type. See {@see setViewType}.
+ */
+ @ViewType
+ public int getViewType() {
+ return mProvider.getViewType_impl();
+ }
+
+ /**
+ * Stops playback and release all the resources. This should be called whenever a VideoView2
+ * instance is no longer to be used.
+ */
+ public void stopPlayback() {
+ mProvider.stopPlayback_impl();
+ }
+
+ /**
+ * Registers a callback to be invoked when the media file is loaded and ready to go.
+ *
+ * @param l the callback that will be run.
+ */
+ public void setOnPreparedListener(OnPreparedListener l) {
+ mProvider.setOnPreparedListener_impl(l);
+ }
+
+ /**
+ * Registers a callback to be invoked when the end of a media file has been reached during
+ * playback.
+ *
+ * @param l the callback that will be run.
+ */
+ public void setOnCompletionListener(OnCompletionListener l) {
+ mProvider.setOnCompletionListener_impl(l);
+ }
+
+ /**
+ * Registers a callback to be invoked when an error occurs during playback or setup. If no
+ * listener is specified, or if the listener returned false, VideoView2 will inform the user of
+ * any errors.
+ *
+ * @param l The callback that will be run
+ */
+ public void setOnErrorListener(OnErrorListener l) {
+ mProvider.setOnErrorListener_impl(l);
+ }
+
+ /**
+ * Registers a callback to be invoked when an informational event occurs during playback or
+ * setup.
+ *
+ * @param l The callback that will be run
+ */
+ public void setOnInfoListener(OnInfoListener l) {
+ mProvider.setOnInfoListener_impl(l);
+ }
+
+ /**
+ * Registers a callback to be invoked when a view type change is done.
+ * {@see #setViewType(int)}
+ * @param l The callback that will be run
+ */
+ public void setOnViewTypeChangedListener(OnViewTypeChangedListener l) {
+ mProvider.setOnViewTypeChangedListener_impl(l);
+ }
+
+ /**
+ * Registers a callback to be invoked when the fullscreen mode should be changed.
+ */
+ public void setFullScreenChangedListener(OnFullScreenChangedListener l) {
+ mProvider.setFullScreenChangedListener_impl(l);
+ }
+
+ /**
+ * Interface definition of a callback to be invoked when the viw type has been changed.
+ */
+ public interface OnViewTypeChangedListener {
+ /**
+ * Called when the view type has been changed.
+ * @see #setViewType(int)
+ * @param viewType
+ * <ul>
+ * <li>{@link #VIEW_TYPE_SURFACEVIEW}
+ * <li>{@link #VIEW_TYPE_TEXTUREVIEW}
+ * </ul>
+ */
+ void onViewTypeChanged(@ViewType int viewType);
+ }
+
+ /**
+ * Interface definition of a callback to be invoked when the media source is ready for playback.
+ */
+ public interface OnPreparedListener {
+ /**
+ * Called when the media file is ready for playback.
+ */
+ void onPrepared();
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when playback of a media source has
+ * completed.
+ */
+ public interface OnCompletionListener {
+ /**
+ * Called when the end of a media source is reached during playback.
+ */
+ void onCompletion();
+ }
+
+ /**
+ * Interface definition of a callback to be invoked when there has been an error during an
+ * asynchronous operation.
+ */
+ public interface OnErrorListener {
+ // TODO: Redefine error codes.
+ /**
+ * Called to indicate an error.
+ * @param what the type of error that has occurred
+ * @param extra an extra code, specific to the error.
+ * @return true if the method handled the error, false if it didn't.
+ * @see MediaPlayer#OnErrorListener
+ */
+ boolean onError(int what, int extra);
+ }
+
+ /**
+ * Interface definition of a callback to be invoked to communicate some info and/or warning
+ * about the media or its playback.
+ */
+ public interface OnInfoListener {
+ /**
+ * Called to indicate an info or a warning.
+ * @param what the type of info or warning.
+ * @param extra an extra code, specific to the info.
+ *
+ * @see MediaPlayer#OnInfoListener
+ */
+ void onInfo(int what, int extra);
+ }
+
+ /**
+ * Interface definition of a callback to be invoked to inform the fullscreen mode is changed.
+ */
+ public interface OnFullScreenChangedListener {
+ /**
+ * Called to indicate a fullscreen mode change.
+ */
+ void onFullScreenChanged(boolean fullScreen);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ mProvider.onAttachedToWindow_impl();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ mProvider.onDetachedFromWindow_impl();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return mProvider.getAccessibilityClassName_impl();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ return mProvider.onTouchEvent_impl(ev);
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent ev) {
+ return mProvider.onTrackballEvent_impl(ev);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return mProvider.onKeyDown_impl(keyCode, event);
+ }
+
+ @Override
+ public void onFinishInflate() {
+ mProvider.onFinishInflate_impl();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ return mProvider.dispatchKeyEvent_impl(event);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ mProvider.setEnabled_impl(enabled);
+ }
+
+ private class SuperProvider implements ViewProvider {
+ @Override
+ public void onAttachedToWindow_impl() {
+ VideoView2.super.onAttachedToWindow();
+ }
+
+ @Override
+ public void onDetachedFromWindow_impl() {
+ VideoView2.super.onDetachedFromWindow();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName_impl() {
+ return VideoView2.super.getAccessibilityClassName();
+ }
+
+ @Override
+ public boolean onTouchEvent_impl(MotionEvent ev) {
+ return VideoView2.super.onTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onTrackballEvent_impl(MotionEvent ev) {
+ return VideoView2.super.onTrackballEvent(ev);
+ }
+
+ @Override
+ public boolean onKeyDown_impl(int keyCode, KeyEvent event) {
+ return VideoView2.super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public void onFinishInflate_impl() {
+ VideoView2.super.onFinishInflate();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent_impl(KeyEvent event) {
+ return VideoView2.super.dispatchKeyEvent(event);
+ }
+
+ @Override
+ public void setEnabled_impl(boolean enabled) {
+ VideoView2.super.setEnabled(enabled);
+ }
+ }
+}