Merge "Use DevicePolicyData#mUserSetupComplete for DPM's isDeviceProvisioned."
diff --git a/api/current.txt b/api/current.txt
index 2c20982..659c1b8 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -601,6 +601,8 @@
field public static final int font = 16844082; // 0x1010532
field public static final int fontFamily = 16843692; // 0x10103ac
field public static final int fontFeatureSettings = 16843959; // 0x10104b7
+ field public static final int fontProviderAuthority = 16844114; // 0x1010552
+ field public static final int fontProviderQuery = 16844115; // 0x1010553
field public static final int fontStyle = 16844081; // 0x1010531
field public static final int fontWeight = 16844083; // 0x1010533
field public static final int footerDividersEnabled = 16843311; // 0x101022f
@@ -52448,6 +52450,13 @@
field public static final java.lang.Class<java.lang.Boolean> TYPE;
}
+ public class BootstrapMethodError extends java.lang.LinkageError {
+ ctor public BootstrapMethodError();
+ ctor public BootstrapMethodError(java.lang.String);
+ ctor public BootstrapMethodError(java.lang.String, java.lang.Throwable);
+ ctor public BootstrapMethodError(java.lang.Throwable);
+ }
+
public final class Byte extends java.lang.Number implements java.lang.Comparable {
ctor public Byte(byte);
ctor public Byte(java.lang.String) throws java.lang.NumberFormatException;
@@ -54334,6 +54343,21 @@
package java.lang.invoke {
+ public abstract class CallSite {
+ method public abstract java.lang.invoke.MethodHandle dynamicInvoker();
+ method public abstract java.lang.invoke.MethodHandle getTarget();
+ method public abstract void setTarget(java.lang.invoke.MethodHandle);
+ method public java.lang.invoke.MethodType type();
+ }
+
+ public class ConstantCallSite extends java.lang.invoke.CallSite {
+ ctor public ConstantCallSite(java.lang.invoke.MethodHandle);
+ ctor protected ConstantCallSite(java.lang.invoke.MethodType, java.lang.invoke.MethodHandle) throws java.lang.Throwable;
+ method public final java.lang.invoke.MethodHandle dynamicInvoker();
+ method public final java.lang.invoke.MethodHandle getTarget();
+ method public final void setTarget(java.lang.invoke.MethodHandle);
+ }
+
public class LambdaConversionException extends java.lang.Exception {
ctor public LambdaConversionException();
ctor public LambdaConversionException(java.lang.String);
@@ -54462,6 +54486,22 @@
method public java.lang.invoke.MethodType wrap();
}
+ public class MutableCallSite extends java.lang.invoke.CallSite {
+ ctor public MutableCallSite(java.lang.invoke.MethodType);
+ ctor public MutableCallSite(java.lang.invoke.MethodHandle);
+ method public final java.lang.invoke.MethodHandle dynamicInvoker();
+ method public final java.lang.invoke.MethodHandle getTarget();
+ method public void setTarget(java.lang.invoke.MethodHandle);
+ }
+
+ public class VolatileCallSite extends java.lang.invoke.CallSite {
+ ctor public VolatileCallSite(java.lang.invoke.MethodType);
+ ctor public VolatileCallSite(java.lang.invoke.MethodHandle);
+ method public final java.lang.invoke.MethodHandle dynamicInvoker();
+ method public final java.lang.invoke.MethodHandle getTarget();
+ method public void setTarget(java.lang.invoke.MethodHandle);
+ }
+
public class WrongMethodTypeException extends java.lang.RuntimeException {
ctor public WrongMethodTypeException();
ctor public WrongMethodTypeException(java.lang.String);
diff --git a/api/system-current.txt b/api/system-current.txt
index af1a705..a6de1ea 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -713,6 +713,8 @@
field public static final int font = 16844082; // 0x1010532
field public static final int fontFamily = 16843692; // 0x10103ac
field public static final int fontFeatureSettings = 16843959; // 0x10104b7
+ field public static final int fontProviderAuthority = 16844114; // 0x1010552
+ field public static final int fontProviderQuery = 16844115; // 0x1010553
field public static final int fontStyle = 16844081; // 0x1010531
field public static final int fontWeight = 16844083; // 0x1010533
field public static final int footerDividersEnabled = 16843311; // 0x101022f
@@ -6892,6 +6894,7 @@
ctor public BackupManager(android.content.Context);
method public void backupNow();
method public android.app.backup.RestoreSession beginRestoreSession();
+ method public void cancelBackups();
method public void dataChanged();
method public static void dataChanged(java.lang.String);
method public long getAvailableRestoreToken(java.lang.String);
@@ -6908,6 +6911,7 @@
method public void setAutoRestore(boolean);
method public void setBackupEnabled(boolean);
field public static final int ERROR_AGENT_FAILURE = -1003; // 0xfffffc15
+ field public static final int ERROR_BACKUP_CANCELLED = -2003; // 0xfffff82d
field public static final int ERROR_BACKUP_NOT_ALLOWED = -2001; // 0xfffff82f
field public static final int ERROR_PACKAGE_NOT_FOUND = -2002; // 0xfffff82e
field public static final int ERROR_TRANSPORT_ABORTED = -1000; // 0xfffffc18
@@ -56248,6 +56252,13 @@
field public static final java.lang.Class<java.lang.Boolean> TYPE;
}
+ public class BootstrapMethodError extends java.lang.LinkageError {
+ ctor public BootstrapMethodError();
+ ctor public BootstrapMethodError(java.lang.String);
+ ctor public BootstrapMethodError(java.lang.String, java.lang.Throwable);
+ ctor public BootstrapMethodError(java.lang.Throwable);
+ }
+
public final class Byte extends java.lang.Number implements java.lang.Comparable {
ctor public Byte(byte);
ctor public Byte(java.lang.String) throws java.lang.NumberFormatException;
@@ -58134,6 +58145,21 @@
package java.lang.invoke {
+ public abstract class CallSite {
+ method public abstract java.lang.invoke.MethodHandle dynamicInvoker();
+ method public abstract java.lang.invoke.MethodHandle getTarget();
+ method public abstract void setTarget(java.lang.invoke.MethodHandle);
+ method public java.lang.invoke.MethodType type();
+ }
+
+ public class ConstantCallSite extends java.lang.invoke.CallSite {
+ ctor public ConstantCallSite(java.lang.invoke.MethodHandle);
+ ctor protected ConstantCallSite(java.lang.invoke.MethodType, java.lang.invoke.MethodHandle) throws java.lang.Throwable;
+ method public final java.lang.invoke.MethodHandle dynamicInvoker();
+ method public final java.lang.invoke.MethodHandle getTarget();
+ method public final void setTarget(java.lang.invoke.MethodHandle);
+ }
+
public class LambdaConversionException extends java.lang.Exception {
ctor public LambdaConversionException();
ctor public LambdaConversionException(java.lang.String);
@@ -58262,6 +58288,22 @@
method public java.lang.invoke.MethodType wrap();
}
+ public class MutableCallSite extends java.lang.invoke.CallSite {
+ ctor public MutableCallSite(java.lang.invoke.MethodType);
+ ctor public MutableCallSite(java.lang.invoke.MethodHandle);
+ method public final java.lang.invoke.MethodHandle dynamicInvoker();
+ method public final java.lang.invoke.MethodHandle getTarget();
+ method public void setTarget(java.lang.invoke.MethodHandle);
+ }
+
+ public class VolatileCallSite extends java.lang.invoke.CallSite {
+ ctor public VolatileCallSite(java.lang.invoke.MethodType);
+ ctor public VolatileCallSite(java.lang.invoke.MethodHandle);
+ method public final java.lang.invoke.MethodHandle dynamicInvoker();
+ method public final java.lang.invoke.MethodHandle getTarget();
+ method public void setTarget(java.lang.invoke.MethodHandle);
+ }
+
public class WrongMethodTypeException extends java.lang.RuntimeException {
ctor public WrongMethodTypeException();
ctor public WrongMethodTypeException(java.lang.String);
diff --git a/api/test-current.txt b/api/test-current.txt
index 43f1a2e..ebba10e 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -601,6 +601,8 @@
field public static final int font = 16844082; // 0x1010532
field public static final int fontFamily = 16843692; // 0x10103ac
field public static final int fontFeatureSettings = 16843959; // 0x10104b7
+ field public static final int fontProviderAuthority = 16844114; // 0x1010552
+ field public static final int fontProviderQuery = 16844115; // 0x1010553
field public static final int fontStyle = 16844081; // 0x1010531
field public static final int fontWeight = 16844083; // 0x1010533
field public static final int footerDividersEnabled = 16843311; // 0x101022f
@@ -52777,6 +52779,13 @@
field public static final java.lang.Class<java.lang.Boolean> TYPE;
}
+ public class BootstrapMethodError extends java.lang.LinkageError {
+ ctor public BootstrapMethodError();
+ ctor public BootstrapMethodError(java.lang.String);
+ ctor public BootstrapMethodError(java.lang.String, java.lang.Throwable);
+ ctor public BootstrapMethodError(java.lang.Throwable);
+ }
+
public final class Byte extends java.lang.Number implements java.lang.Comparable {
ctor public Byte(byte);
ctor public Byte(java.lang.String) throws java.lang.NumberFormatException;
@@ -54663,6 +54672,21 @@
package java.lang.invoke {
+ public abstract class CallSite {
+ method public abstract java.lang.invoke.MethodHandle dynamicInvoker();
+ method public abstract java.lang.invoke.MethodHandle getTarget();
+ method public abstract void setTarget(java.lang.invoke.MethodHandle);
+ method public java.lang.invoke.MethodType type();
+ }
+
+ public class ConstantCallSite extends java.lang.invoke.CallSite {
+ ctor public ConstantCallSite(java.lang.invoke.MethodHandle);
+ ctor protected ConstantCallSite(java.lang.invoke.MethodType, java.lang.invoke.MethodHandle) throws java.lang.Throwable;
+ method public final java.lang.invoke.MethodHandle dynamicInvoker();
+ method public final java.lang.invoke.MethodHandle getTarget();
+ method public final void setTarget(java.lang.invoke.MethodHandle);
+ }
+
public class LambdaConversionException extends java.lang.Exception {
ctor public LambdaConversionException();
ctor public LambdaConversionException(java.lang.String);
@@ -54791,6 +54815,22 @@
method public java.lang.invoke.MethodType wrap();
}
+ public class MutableCallSite extends java.lang.invoke.CallSite {
+ ctor public MutableCallSite(java.lang.invoke.MethodType);
+ ctor public MutableCallSite(java.lang.invoke.MethodHandle);
+ method public final java.lang.invoke.MethodHandle dynamicInvoker();
+ method public final java.lang.invoke.MethodHandle getTarget();
+ method public void setTarget(java.lang.invoke.MethodHandle);
+ }
+
+ public class VolatileCallSite extends java.lang.invoke.CallSite {
+ ctor public VolatileCallSite(java.lang.invoke.MethodType);
+ ctor public VolatileCallSite(java.lang.invoke.MethodHandle);
+ method public final java.lang.invoke.MethodHandle dynamicInvoker();
+ method public final java.lang.invoke.MethodHandle getTarget();
+ method public void setTarget(java.lang.invoke.MethodHandle);
+ }
+
public class WrongMethodTypeException extends java.lang.RuntimeException {
ctor public WrongMethodTypeException();
ctor public WrongMethodTypeException(java.lang.String);
diff --git a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
index a67e47f..bfcad1b 100644
--- a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
+++ b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
@@ -127,6 +127,11 @@
return;
}
+ if ("cancel".equals(op)) {
+ doCancel();
+ return;
+ }
+
if ("whitelist".equals(op)) {
doPrintWhitelist();
return;
@@ -270,6 +275,8 @@
return "Agent error";
case BackupManager.ERROR_TRANSPORT_QUOTA_EXCEEDED:
return "Size quota exceeded";
+ case BackupManager.ERROR_BACKUP_CANCELLED:
+ return "Backup Cancelled";
default:
return "Unknown error";
}
@@ -361,6 +368,21 @@
}
}
+ private void doCancel() {
+ String arg = nextArg();
+ if ("backups".equals(arg)) {
+ try {
+ mBmgr.cancelBackups();
+ } catch (RemoteException e) {
+ System.err.println(e.toString());
+ System.err.println(BMGR_NOT_RUNNING_ERR);
+ }
+ return;
+ }
+
+ System.err.println("Unknown command.");
+ }
+
private void doTransport() {
try {
String which = nextArg();
@@ -721,6 +743,7 @@
System.err.println(" bmgr wipe TRANSPORT PACKAGE");
System.err.println(" bmgr fullbackup PACKAGE...");
System.err.println(" bmgr backupnow --all|PACKAGE...");
+ System.err.println(" bmgr cancel backups");
System.err.println("");
System.err.println("The 'backup' command schedules a backup pass for the named package.");
System.err.println("Note that the backup pass will effectively be a no-op if the package");
@@ -780,5 +803,6 @@
System.err.println("For each package it will run key/value or full data backup ");
System.err.println("depending on the package's manifest declarations.");
System.err.println("The data is sent via the currently active transport.");
+ System.err.println("The 'cancel backups' command cancels all running backups.");
}
}
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 50fee83..376823e 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -5722,6 +5722,24 @@
} finally {
StrictMode.setThreadPolicy(savedPolicy);
}
+
+ // Preload fonts resources
+ try {
+ final ApplicationInfo info =
+ sPackageManager.getApplicationInfo(
+ data.appInfo.packageName,
+ PackageManager.GET_META_DATA /*flags*/,
+ UserHandle.myUserId());
+ if (info.metaData != null) {
+ final int preloadedFontsResource = info.metaData.getInt(
+ ApplicationInfo.METADATA_PRELOADED_FONTS, 0);
+ if (preloadedFontsResource != 0) {
+ data.info.mResources.preloadFonts(preloadedFontsResource);
+ }
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/*package*/ final void finishInstrumentation(int resultCode, Bundle results) {
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 6719be4..6717491 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -210,6 +210,7 @@
boolean killPids(in int[] pids, in String reason, boolean secure);
List<ActivityManager.RunningServiceInfo> getServices(int maxNum, int flags);
ActivityManager.TaskThumbnail getTaskThumbnail(int taskId);
+ ActivityManager.TaskDescription getTaskDescription(int taskId);
// Retrieve running application processes in the system
List<ActivityManager.RunningAppProcessInfo> getRunningAppProcesses();
// Get device configuration
diff --git a/core/java/android/app/QueuedWork.java b/core/java/android/app/QueuedWork.java
index 0ae8505..6ee4780 100644
--- a/core/java/android/app/QueuedWork.java
+++ b/core/java/android/app/QueuedWork.java
@@ -16,197 +16,86 @@
package android.app;
-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 com.android.internal.annotations.GuardedBy;
-
-import java.util.LinkedList;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
/**
- * Internal utility class to keep track of process-global work that's outstanding and hasn't been
- * finished yet.
+ * Internal utility class to keep track of process-global work that's
+ * outstanding and hasn't been finished yet.
*
- * New work will be {@link #queue queued}.
- *
- * It is possible to add 'finisher'-runnables that are {@link #waitToFinish guaranteed to be run}.
- * This is used to make sure the work has been finished.
- *
- * This was created for writing SharedPreference edits out asynchronously so we'd have a mechanism
- * to wait for the writes in Activity.onPause and similar places, but we may use this mechanism for
- * other things in the future.
- *
- * The queued asynchronous work is performed on a separate, dedicated thread.
+ * This was created for writing SharedPreference edits out
+ * asynchronously so we'd have a mechanism to wait for the writes in
+ * Activity.onPause and similar places, but we may use this mechanism
+ * for other things in the future.
*
* @hide
*/
public class QueuedWork {
- private static final String LOG_TAG = QueuedWork.class.getSimpleName();
- /** Delay for delayed runnables */
- private static final long DELAY = 50;
+ // The set of Runnables that will finish or wait on any async
+ // activities started by the application.
+ private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =
+ new ConcurrentLinkedQueue<Runnable>();
- /** Lock for this class */
- private static final Object sLock = new Object();
-
- /** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */
- @GuardedBy("sLock")
- private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
-
- /** {@link #getHandler() Lazily} created handler */
- @GuardedBy("sLock")
- private static Handler sHandler = null;
-
- /** Work queued via {@link #queue} */
- @GuardedBy("sLock")
- private static final LinkedList<Runnable> sWork = new LinkedList<>();
-
- /** If new work can be delayed or not */
- @GuardedBy("sLock")
- private static boolean sCanDelay = true;
+ private static ExecutorService sSingleThreadExecutor = null; // lazy, guarded by class
/**
- * Lazily create a handler on a separate thread.
- *
- * @return the handler
+ * Returns a single-thread Executor shared by the entire process,
+ * creating it if necessary.
*/
- private static Handler getHandler() {
- synchronized (sLock) {
- if (sHandler == null) {
- HandlerThread handlerThread = new HandlerThread("queued-work-looper",
- Process.THREAD_PRIORITY_BACKGROUND);
- handlerThread.start();
-
- sHandler = new QueuedWorkHandler(handlerThread.getLooper());
+ public static ExecutorService singleThreadExecutor() {
+ synchronized (QueuedWork.class) {
+ if (sSingleThreadExecutor == null) {
+ // TODO: can we give this single thread a thread name?
+ sSingleThreadExecutor = Executors.newSingleThreadExecutor();
}
- return sHandler;
+ return sSingleThreadExecutor;
}
}
/**
- * Add a finisher-runnable to wait for {@link #queue asynchronously processed work}.
+ * Add a runnable to finish (or wait for) a deferred operation
+ * started in this context earlier. Typically finished by e.g.
+ * an Activity#onPause. Used by SharedPreferences$Editor#startCommit().
*
- * Used by SharedPreferences$Editor#startCommit().
- *
- * Note that this doesn't actually start it running. This is just a scratch set for callers
- * doing async work to keep updated with what's in-flight. In the common case, caller code
- * (e.g. SharedPreferences) will pretty quickly call remove() after an add(). The only time
- * these Runnables are run is from {@link #waitToFinish}.
- *
- * @param finisher The runnable to add as finisher
+ * Note that this doesn't actually start it running. This is just
+ * a scratch set for callers doing async work to keep updated with
+ * what's in-flight. In the common case, caller code
+ * (e.g. SharedPreferences) will pretty quickly call remove()
+ * after an add(). The only time these Runnables are run is from
+ * waitToFinish(), below.
*/
- public static void addFinisher(Runnable finisher) {
- synchronized (sLock) {
- sFinishers.add(finisher);
- }
+ public static void add(Runnable finisher) {
+ sPendingWorkFinishers.add(finisher);
+ }
+
+ public static void remove(Runnable finisher) {
+ sPendingWorkFinishers.remove(finisher);
}
/**
- * Remove a previously {@link #addFinisher added} finisher-runnable.
+ * Finishes or waits for async operations to complete.
+ * (e.g. SharedPreferences$Editor#startCommit writes)
*
- * @param finisher The runnable to remove.
- */
- public static void removeFinisher(Runnable finisher) {
- synchronized (sLock) {
- sFinishers.remove(finisher);
- }
- }
-
- /**
- * Trigger queued work to be processed immediately. The queued work is processed on a separate
- * thread asynchronous. While doing that run and process all finishers on this thread. The
- * finishers can be implemented in a way to check weather the queued work is finished.
- *
- * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
- * after Service command handling, etc. (so async work is never lost)
+ * Is called from the Activity base class's onPause(), after
+ * BroadcastReceiver's onReceive, after Service command handling,
+ * etc. (so async work is never lost)
*/
public static void waitToFinish() {
- Handler handler = getHandler();
-
- synchronized (sLock) {
- if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
- // Force the delayed work to be processed now
- handler.removeMessages(QueuedWorkHandler.MSG_RUN);
- handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
- }
-
- // We should not delay any work as this might delay the finishers
- sCanDelay = false;
- }
-
- try {
- while (true) {
- Runnable finisher;
-
- synchronized (sLock) {
- finisher = sFinishers.poll();
- }
-
- if (finisher == null) {
- break;
- }
-
- finisher.run();
- }
- } finally {
- sCanDelay = true;
+ Runnable toFinish;
+ while ((toFinish = sPendingWorkFinishers.poll()) != null) {
+ toFinish.run();
}
}
-
+
/**
- * Queue a work-runnable for processing asynchronously.
- *
- * @param work The new runnable to process
- * @param shouldDelay If the message should be delayed
- */
- public static void queue(Runnable work, boolean shouldDelay) {
- Handler handler = getHandler();
-
- synchronized (sLock) {
- sWork.add(work);
-
- if (shouldDelay && sCanDelay) {
- handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
- } else {
- handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
- }
- }
- }
-
- /**
- * @return True iff there is any {@link #queue async work queued}.
+ * Returns true if there is pending work to be done. Note that the
+ * result is out of data as soon as you receive it, so be careful how you
+ * use it.
*/
public static boolean hasPendingWork() {
- synchronized (sLock) {
- return !sWork.isEmpty();
- }
+ return !sPendingWorkFinishers.isEmpty();
}
-
- private static class QueuedWorkHandler extends Handler {
- static final int MSG_RUN = 1;
-
- QueuedWorkHandler(Looper looper) {
- super(looper);
- }
-
- public void handleMessage(Message msg) {
- if (msg.what == MSG_RUN) {
- LinkedList<Runnable> work;
-
- synchronized (sWork) {
- work = (LinkedList<Runnable>) sWork.clone();
- sWork.clear();
-
- // Remove all msg-s as all work will be processed now
- removeMessages(MSG_RUN);
- }
-
- work.forEach(Runnable::run);
- }
- }
- }
+
}
diff --git a/core/java/android/app/SharedPreferencesImpl.java b/core/java/android/app/SharedPreferencesImpl.java
index 023b4f3..3bb7019 100644
--- a/core/java/android/app/SharedPreferencesImpl.java
+++ b/core/java/android/app/SharedPreferencesImpl.java
@@ -406,12 +406,12 @@
}
};
- QueuedWork.addFinisher(awaitCommit);
+ QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
- QueuedWork.removeFinisher(awaitCommit);
+ QueuedWork.remove(awaitCommit);
}
};
@@ -588,10 +588,10 @@
}
if (DEBUG) {
- Log.d(TAG, "queued " + mcr.memoryStateGeneration + " -> " + mFile.getName());
+ Log.d(TAG, "added " + mcr.memoryStateGeneration + " -> " + mFile.getName());
}
- QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
+ QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
private static FileOutputStream createFileOutputStream(File file) {
diff --git a/core/java/android/app/backup/BackupManager.java b/core/java/android/app/backup/BackupManager.java
index 59bd01f..9d02f53 100644
--- a/core/java/android/app/backup/BackupManager.java
+++ b/core/java/android/app/backup/BackupManager.java
@@ -89,6 +89,14 @@
public static final int ERROR_PACKAGE_NOT_FOUND = -2002;
/**
+ * The backup operation was cancelled.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ERROR_BACKUP_CANCELLED = -2003;
+
+ /**
* The transport for some reason was not in a good state and
* aborted the entire backup request. This is a transient
* failure and should not be retried immediately.
@@ -626,6 +634,26 @@
return -1;
}
+ /**
+ * Cancel all running backups. After this call returns, no currently running backups will
+ * interact with the selected transport.
+ *
+ * <p>Callers must hold the android.permission.BACKUP permission to use this method.
+ *
+ * @hide
+ */
+ @SystemApi
+ public void cancelBackups() {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ sService.cancelBackups();
+ } catch (RemoteException e) {
+ Log.e(TAG, "cancelBackups() couldn't connect.");
+ }
+ }
+ }
+
/*
* We wrap incoming binder calls with a private class implementation that
* redirects them into main-thread actions. This serializes the backup
diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl
index 393667d..59a941a 100644
--- a/core/java/android/app/backup/IBackupManager.aidl
+++ b/core/java/android/app/backup/IBackupManager.aidl
@@ -386,4 +386,10 @@
*/
int requestBackup(in String[] packages, IBackupObserver observer, IBackupManagerMonitor monitor,
int flags);
+
+ /**
+ * Cancel all running backups. After this call returns, no currently running backups will
+ * interact with the selected transport.
+ */
+ void cancelBackups();
}
diff --git a/core/java/android/content/BroadcastReceiver.java b/core/java/android/content/BroadcastReceiver.java
index c3d6606..485d078 100644
--- a/core/java/android/content/BroadcastReceiver.java
+++ b/core/java/android/content/BroadcastReceiver.java
@@ -211,16 +211,16 @@
// of the list to finish the broadcast, so we don't block this
// thread (which may be the main thread) to have it finished.
//
- // Note that we don't need to use QueuedWork.addFinisher() with the
+ // Note that we don't need to use QueuedWork.add() with the
// runnable, since we know the AM is waiting for us until the
// executor gets to it.
- QueuedWork.queue(new Runnable() {
+ QueuedWork.singleThreadExecutor().execute( new Runnable() {
@Override public void run() {
if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG,
"Finishing broadcast after work to component " + mToken);
sendFinished(mgr);
}
- }, false);
+ });
} else {
if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG,
"Finishing broadcast to component " + mToken);
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index 8465f0f..1fa4181 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -573,6 +573,11 @@
public int privateFlags;
/**
+ * @hide
+ */
+ public static final String METADATA_PRELOADED_FONTS = "preloaded_fonts";
+
+ /**
* The required smallest screen width the application can run on. If 0,
* nothing has been specified. Comes from
* {@link android.R.styleable#AndroidManifestSupportsScreens_requiresSmallestWidthDp
diff --git a/core/java/android/content/res/FontResourcesParser.java b/core/java/android/content/res/FontResourcesParser.java
index 7ea62db..3f8f90e 100644
--- a/core/java/android/content/res/FontResourcesParser.java
+++ b/core/java/android/content/res/FontResourcesParser.java
@@ -64,6 +64,17 @@
private static FontConfig.Family readFamily(XmlPullParser parser, Resources resources)
throws XmlPullParserException, IOException {
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ TypedArray array = resources.obtainAttributes(attrs, R.styleable.FontFamily);
+ String authority = array.getString(R.styleable.FontFamily_fontProviderAuthority);
+ String query = array.getString(R.styleable.FontFamily_fontProviderQuery);
+ array.recycle();
+ if (authority != null && query != null) {
+ while (parser.next() != XmlPullParser.END_TAG) {
+ skip(parser);
+ }
+ return new FontConfig.Family(authority, query);
+ }
List<FontConfig.Font> fonts = new ArrayList<>();
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
@@ -84,11 +95,12 @@
int weight = array.getInt(R.styleable.FontFamilyFont_fontWeight, NORMAL_WEIGHT);
boolean isItalic = ITALIC == array.getInt(R.styleable.FontFamilyFont_fontStyle, 0);
String filename = array.getString(R.styleable.FontFamilyFont_font);
+ int resourceId = array.getResourceId(R.styleable.FontFamilyFont_font, 0);
array.recycle();
while (parser.next() != XmlPullParser.END_TAG) {
skip(parser);
}
- return new FontConfig.Font(filename, 0, null, weight, isItalic);
+ return new FontConfig.Font(filename, 0, null, weight, isItalic, resourceId);
}
private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java
index 04e4454..21d4b22 100644
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -373,6 +373,20 @@
}
/**
+ * @hide
+ */
+ public void preloadFonts(@FontRes int id) {
+ final TypedValue value = obtainTempTypedValue();
+ try {
+ final ResourcesImpl impl = mResourcesImpl;
+ impl.getValue(id, value, true);
+ impl.preloadFonts(this, value, id);
+ } finally {
+ releaseTempTypedValue(value);
+ }
+ }
+
+ /**
* Returns the character sequence necessary for grammatically correct pluralization
* of the given resource ID for the given quantity.
* Note that the character sequence is selected based solely on grammatical necessity,
diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java
index bf81096..38efa49 100644
--- a/core/java/android/content/res/ResourcesImpl.java
+++ b/core/java/android/content/res/ResourcesImpl.java
@@ -32,6 +32,7 @@
import android.content.pm.ActivityInfo.Config;
import android.content.res.Configuration.NativeConfig;
import android.content.res.Resources.NotFoundException;
+import android.graphics.FontFamily;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
@@ -52,6 +53,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
+import java.util.List;
import java.util.Locale;
/**
@@ -782,6 +784,43 @@
}
/**
+ * @hide
+ */
+ public void preloadFonts(Resources wrapper, TypedValue value, int id) {
+ if (value.string == null) {
+ throw new NotFoundException("Resource \"" + getResourceName(id) + "\" ("
+ + Integer.toHexString(id) + ") is not a Font: " + value);
+ }
+
+ final String file = value.string.toString();
+
+ Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
+ try {
+ final XmlResourceParser rp = loadXmlResourceParser(
+ file, id, value.assetCookie, "font");
+ final FontConfig config = FontResourcesParser.parse(rp, wrapper);
+ final List<FontConfig.Family> families = config.getFamilies();
+ if (families == null || families.isEmpty()) {
+ return;
+ }
+ for (int j = 0; j < families.size(); j++) {
+ final FontConfig.Family family = families.get(j);
+ final List<FontConfig.Font> fonts = family.getFonts();
+ for (int i = 0; i < fonts.size(); i++) {
+ int resourceId = fonts.get(i).getResourceId();
+ wrapper.getFont(resourceId);
+ }
+ }
+ } catch (XmlPullParserException e) {
+ Log.e(TAG, "Failed to parse xml resource " + file, e);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to read xml resource " + file, e);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+ }
+ }
+
+ /**
* Given the value and id, we can get the XML filename as in value.data, based on that, we
* first try to load CSL from the cache. If not found, try to get from the constant state.
* Last, parse the XML and generate the CSL.
diff --git a/core/java/android/text/FontConfig.java b/core/java/android/text/FontConfig.java
index 3048a38..82e44dc 100644
--- a/core/java/android/text/FontConfig.java
+++ b/core/java/android/text/FontConfig.java
@@ -170,14 +170,24 @@
private final int mWeight;
private final boolean mIsItalic;
private ParcelFileDescriptor mFd;
+ private final int mResourceId;
- public Font(String fontName, int ttcIndex, List<Axis> axes, int weight, boolean isItalic) {
+ /**
+ * @hide
+ */
+ public Font(String fontName, int ttcIndex, List<Axis> axes, int weight, boolean isItalic,
+ int resourceId) {
mFontName = fontName;
mTtcIndex = ttcIndex;
mAxes = axes;
mWeight = weight;
mIsItalic = isItalic;
mFd = null;
+ mResourceId = resourceId;
+ }
+
+ public Font(String fontName, int ttcIndex, List<Axis> axes, int weight, boolean isItalic) {
+ this(fontName, ttcIndex, axes, weight, isItalic, 0);
}
public Font(Font origin) {
@@ -193,6 +203,7 @@
e.printStackTrace();
}
}
+ mResourceId = origin.mResourceId;
}
/**
@@ -254,6 +265,13 @@
/**
* @hide
*/
+ public int getResourceId() {
+ return mResourceId;
+ }
+
+ /**
+ * @hide
+ */
public Font(Parcel in) {
mFontName = in.readString();
mTtcIndex = in.readInt();
@@ -269,6 +287,7 @@
} else {
mFd = null;
}
+ mResourceId = in.readInt();
}
@Override
@@ -285,6 +304,7 @@
if (mFd != null) {
mFd.writeToParcel(out, flag);
}
+ out.writeInt(mResourceId);
}
@Override
@@ -382,22 +402,40 @@
private final List<Font> mFonts;
private final String mLanguage;
private final String mVariant;
+ private final String mProviderAuthority;
+ private final String mQuery;
public Family(String name, List<Font> fonts, String language, String variant) {
- this.mName = name;
- this.mFonts = fonts;
- this.mLanguage = language;
- this.mVariant = variant;
+ mName = name;
+ mFonts = fonts;
+ mLanguage = language;
+ mVariant = variant;
+ mProviderAuthority = null;
+ mQuery = null;
+ }
+
+ /**
+ * @hide
+ */
+ public Family(String providerAuthority, String query) {
+ mName = null;
+ mFonts = null;
+ mLanguage = null;
+ mVariant = null;
+ mProviderAuthority = providerAuthority;
+ mQuery = query;
}
public Family(Family origin) {
- this.mName = origin.mName;
- this.mLanguage = origin.mLanguage;
- this.mVariant = origin.mVariant;
- this.mFonts = new ArrayList<>();
+ mName = origin.mName;
+ mLanguage = origin.mLanguage;
+ mVariant = origin.mVariant;
+ mFonts = new ArrayList<>();
for (int i = 0; i < origin.mFonts.size(); i++) {
mFonts.add(new Font(origin.mFonts.get(i)));
}
+ mProviderAuthority = origin.mProviderAuthority;
+ mQuery = origin.mQuery;
}
/**
@@ -431,6 +469,20 @@
/**
* @hide
*/
+ public String getProviderAuthority() {
+ return mProviderAuthority;
+ }
+
+ /**
+ * @hide
+ */
+ public String getQuery() {
+ return mQuery;
+ }
+
+ /**
+ * @hide
+ */
public Family(Parcel in) {
mName = in.readString();
final int size = in.readInt();
@@ -440,6 +492,16 @@
}
mLanguage = in.readString();
mVariant = in.readString();
+ if (in.readInt() == 1) {
+ mProviderAuthority = in.readString();
+ } else {
+ mProviderAuthority = null;
+ }
+ if (in.readInt() == 1) {
+ mQuery = in.readString();
+ } else {
+ mQuery = null;
+ }
}
@Override
@@ -451,6 +513,14 @@
}
out.writeString(mLanguage);
out.writeString(mVariant);
+ out.writeInt(mProviderAuthority == null ? 0 : 1);
+ if (mProviderAuthority != null) {
+ out.writeString(mProviderAuthority);
+ }
+ out.writeInt(mQuery == null ? 0 : 1);
+ if (mQuery != null) {
+ out.writeString(mQuery);
+ }
}
@Override
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index d8fc1d1..36bb821 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -8507,6 +8507,12 @@
<attr name="fontWeight" format="integer" />
</declare-styleable>
+ <!-- Attributes that are read when parsing a <fontfamily> tag, -->
+ <declare-styleable name="FontFamily">
+ <attr name="fontProviderAuthority" format="string" />
+ <attr name="fontProviderQuery" format="string" />
+ </declare-styleable>
+
<!-- @hide -->
<declare-styleable name="RecyclerView">
<attr name="layoutManager" format="string" />
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 34659aa..78489eb 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2794,6 +2794,8 @@
<public name="canCaptureFingerprintGestures" />
<public name="alphabeticModifiers" />
<public name="numericModifiers" />
+ <public name="fontProviderAuthority" />
+ <public name="fontProviderQuery" />
</public-group>
<public-group type="style" first-id="0x010302e0">
@@ -2806,6 +2808,7 @@
<public type="attr" name="primaryContentAlpha" />
<public type="attr" name="secondaryContentAlpha" />
+
<!-- ===============================================================
DO NOT ADD UN-GROUPED ITEMS HERE
diff --git a/core/tests/coretests/res/font/samplexmldownloadedfont.xml b/core/tests/coretests/res/font/samplexmldownloadedfont.xml
new file mode 100644
index 0000000..35391bd
--- /dev/null
+++ b/core/tests/coretests/res/font/samplexmldownloadedfont.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<font-family xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fontProviderAuthority="com.example.test.fontprovider"
+ android:fontProviderQuery="MyRequestedFont">
+</font-family>
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/content/res/FontResourcesParserTest.java b/core/tests/coretests/src/android/content/res/FontResourcesParserTest.java
index 380a28774..8b536a7 100644
--- a/core/tests/coretests/src/android/content/res/FontResourcesParserTest.java
+++ b/core/tests/coretests/src/android/content/res/FontResourcesParserTest.java
@@ -15,6 +15,8 @@
*/
package android.content.res;
+import static junit.framework.Assert.assertNull;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -78,4 +80,19 @@
assertEquals(true, font4.isItalic());
assertEquals("res/font/samplefont4.ttf", font4.getFontName());
}
+
+ @Test
+ public void testParseDownloadableFont() throws IOException, XmlPullParserException {
+ XmlResourceParser parser = mResources.getXml(R.font.samplexmldownloadedfont);
+
+ FontConfig result = FontResourcesParser.parse(parser, mResources);
+
+ assertNotNull(result);
+ List<FontConfig.Family> families = result.getFamilies();
+ assertEquals(1, families.size());
+ FontConfig.Family family = families.get(0);
+ assertEquals("com.example.test.fontprovider", family.getProviderAuthority());
+ assertEquals("MyRequestedFont", family.getQuery());
+ assertNull(family.getFonts());
+ }
}
diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java
index 750ef3f..7eb8099 100644
--- a/graphics/java/android/graphics/Typeface.java
+++ b/graphics/java/android/graphics/Typeface.java
@@ -163,36 +163,54 @@
@Nullable
public static Typeface createFromResources(FontConfig config, AssetManager mgr, String path) {
if (sFallbackFonts != null) {
+ Typeface typeface = findFromCache(mgr, path);
+ if (typeface != null) return typeface;
+
+ List<FontConfig.Family> families = config.getFamilies();
+ if (families == null || families.isEmpty()) {
+ throw new RuntimeException(
+ "Font resource " + path + " contained no font families.");
+ }
+ if (families.size() > 1) {
+ throw new RuntimeException(
+ "Font resource " + path + " contained more than one family.");
+ }
+ FontConfig.Family family = families.get(0);
+ if (family.getProviderAuthority() != null && family.getQuery() != null) {
+ // Downloadable font
+ typeface = findFromCache(
+ family.getProviderAuthority(), family.getQuery());
+ if (typeface != null) {
+ return typeface;
+ }
+ // Downloaded font and it wasn't cached, request it again and return a
+ // default font instead (nothing we can do now).
+ create(new FontRequest(family.getProviderAuthority(), family.getQuery()),
+ NO_OP_REQUEST_CALLBACK);
+ return DEFAULT;
+ }
+
+ FontFamily fontFamily = new FontFamily();
+ List<FontConfig.Font> fonts = family.getFonts();
+ if (fonts == null || fonts.isEmpty()) {
+ throw new RuntimeException("Font resource " + path + " contained no fonts.");
+ }
+ for (int i = 0; i < fonts.size(); i++) {
+ FontConfig.Font font = fonts.get(i);
+ // TODO: Use style and weight info
+ if (!fontFamily.addFontFromAssetManager(mgr, font.getFontName(),
+ 0 /* resourceCookie */, false /* isAsset */)) {
+ return null;
+ }
+ }
+ fontFamily.freeze();
+ FontFamily[] familyChain = { fontFamily };
+ typeface = createFromFamiliesWithDefault(familyChain);
synchronized (sDynamicTypefaceCache) {
final String key = createAssetUid(mgr, path);
- Typeface typeface = sDynamicTypefaceCache.get(key);
- if (typeface != null) return typeface;
-
- List<FontConfig.Family> families = config.getFamilies();
- if (families == null || families.isEmpty()) {
- throw new RuntimeException("Font resource contained no fonts.");
- }
- if (families.size() > 1) {
- throw new RuntimeException("Font resource contained more than one family.");
- }
- FontConfig.Family family = families.get(0);
-
- FontFamily fontFamily = new FontFamily();
- List<FontConfig.Font> fonts = family.getFonts();
- for (int i = 0; i < fonts.size(); i++) {
- FontConfig.Font font = fonts.get(i);
- // TODO: Use style and weight info
- if (!fontFamily.addFontFromAssetManager(mgr, font.getFontName(),
- 0 /* resourceCookie */, false /* isAsset */)) {
- return null;
- }
- }
- fontFamily.freeze();
- FontFamily[] familyChain = { fontFamily };
- typeface = createFromFamiliesWithDefault(familyChain);
sDynamicTypefaceCache.put(key, typeface);
- return typeface;
}
+ return typeface;
}
return null;
}
@@ -372,6 +390,18 @@
void onTypefaceRequestFailed(@FontRequestFailReason int reason);
}
+ private static final FontRequestCallback NO_OP_REQUEST_CALLBACK = new FontRequestCallback() {
+ @Override
+ public void onTypefaceRetrieved(Typeface typeface) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onTypefaceRequestFailed(@FontRequestFailReason int reason) {
+ // Do nothing.
+ }
+ };
+
/**
* Create a typeface object given a family name, and option style information.
* If null is passed for the name, then the "default" font will be chosen.
diff --git a/keystore/java/android/security/KeyChain.java b/keystore/java/android/security/KeyChain.java
index e566b9d..9981668 100644
--- a/keystore/java/android/security/KeyChain.java
+++ b/keystore/java/android/security/KeyChain.java
@@ -588,7 +588,7 @@
* @hide for reuse by CertInstaller and Settings.
* @see KeyChain#bind
*/
- public final static class KeyChainConnection implements Closeable {
+ public static class KeyChainConnection implements Closeable {
private final Context context;
private final ServiceConnection serviceConnection;
private final IKeyChainService service;
diff --git a/keystore/java/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java b/keystore/java/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java
index 9701b0e..988e32c 100644
--- a/keystore/java/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java
+++ b/keystore/java/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java
@@ -200,6 +200,11 @@
}
if (mKeymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC) {
+ if (mKeySizeBits < 64) {
+ throw new InvalidAlgorithmParameterException(
+ "HMAC key size must be at least 64 bits.");
+ }
+
// JCA HMAC key algorithm implies a digest (e.g., HmacSHA256 key algorithm
// implies SHA-256 digest). Because keymaster HMAC key is authorized only for
// one digest, we don't let algorithm parameter spec override the digest implied
diff --git a/libs/hwui/tests/common/scenes/BitmapShaders.cpp b/libs/hwui/tests/common/scenes/BitmapShaders.cpp
index e03c9e8..a7ebb68 100644
--- a/libs/hwui/tests/common/scenes/BitmapShaders.cpp
+++ b/libs/hwui/tests/common/scenes/BitmapShaders.cpp
@@ -48,21 +48,17 @@
SkBitmap bitmap;
SkPaint paint;
hwuiBitmap->getSkBitmapForShaders(&bitmap);
-
- sk_sp<SkShader> repeatShader = SkMakeBitmapShader(bitmap,
+ sk_sp<SkImage> image = SkMakeImageFromRasterBitmap(bitmap, kNever_SkCopyPixelsMode);
+ sk_sp<SkShader> repeatShader = image->makeShader(
SkShader::TileMode::kRepeat_TileMode,
SkShader::TileMode::kRepeat_TileMode,
- nullptr,
- kNever_SkCopyPixelsMode,
nullptr);
paint.setShader(std::move(repeatShader));
canvas.drawRoundRect(0, 0, 500, 500, 50.0f, 50.0f, paint);
- sk_sp<SkShader> mirrorShader = SkMakeBitmapShader(bitmap,
+ sk_sp<SkShader> mirrorShader = image->makeShader(
SkShader::TileMode::kMirror_TileMode,
SkShader::TileMode::kMirror_TileMode,
- nullptr,
- kNever_SkCopyPixelsMode,
nullptr);
paint.setShader(std::move(mirrorShader));
canvas.drawRoundRect(0, 600, 500, 1100, 50.0f, 50.0f, paint);
diff --git a/libs/hwui/tests/unit/RecordingCanvasTests.cpp b/libs/hwui/tests/unit/RecordingCanvasTests.cpp
index 124f5fa..669f03c 100644
--- a/libs/hwui/tests/unit/RecordingCanvasTests.cpp
+++ b/libs/hwui/tests/unit/RecordingCanvasTests.cpp
@@ -748,11 +748,10 @@
SkPaint paint;
SkBitmap skBitmap;
bitmap->getSkBitmap(&skBitmap);
- sk_sp<SkShader> shader = SkMakeBitmapShader(skBitmap,
+ sk_sp<SkImage> image = SkMakeImageFromRasterBitmap(skBitmap, kNever_SkCopyPixelsMode);
+ sk_sp<SkShader> shader = image->makeShader(
SkShader::TileMode::kClamp_TileMode,
SkShader::TileMode::kClamp_TileMode,
- nullptr,
- kNever_SkCopyPixelsMode,
nullptr);
paint.setShader(std::move(shader));
canvas.drawRoundRect(0, 0, 100, 100, 20.0f, 20.0f, paint);
@@ -767,11 +766,10 @@
SkPaint paint;
SkBitmap skBitmap;
bitmap->getSkBitmap(&skBitmap);
- sk_sp<SkShader> shader1 = SkMakeBitmapShader(skBitmap,
+ sk_sp<SkImage> image = SkMakeImageFromRasterBitmap(skBitmap, kNever_SkCopyPixelsMode);
+ sk_sp<SkShader> shader1 = image->makeShader(
SkShader::TileMode::kClamp_TileMode,
SkShader::TileMode::kClamp_TileMode,
- nullptr,
- kNever_SkCopyPixelsMode,
nullptr);
SkPoint center;
diff --git a/libs/hwui/tests/unit/SkiaBehaviorTests.cpp b/libs/hwui/tests/unit/SkiaBehaviorTests.cpp
index 03e6b7f..7ae58a6 100644
--- a/libs/hwui/tests/unit/SkiaBehaviorTests.cpp
+++ b/libs/hwui/tests/unit/SkiaBehaviorTests.cpp
@@ -39,12 +39,10 @@
*/
TEST(SkiaBehavior, CreateBitmapShader1x1) {
SkBitmap origBitmap = createSkBitmap(1, 1);
- sk_sp<SkShader> s = SkMakeBitmapShader(
- origBitmap,
+ sk_sp<SkImage> image = SkMakeImageFromRasterBitmap(origBitmap, kNever_SkCopyPixelsMode);
+ sk_sp<SkShader> s = image->makeShader(
SkShader::kClamp_TileMode,
SkShader::kRepeat_TileMode,
- nullptr,
- kNever_SkCopyPixelsMode,
nullptr);
SkBitmap bitmap;
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index 561d924..1f03b51 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -1504,4 +1504,20 @@
return isMusicApp;
}
};
+
+ public static final AppFilter FILTER_OTHER_APPS = new AppFilter() {
+ @Override
+ public void init() {
+ }
+
+ @Override
+ public boolean filterApp(AppEntry entry) {
+ boolean isCategorized;
+ synchronized(entry) {
+ isCategorized = entry.info.category == ApplicationInfo.CATEGORY_AUDIO ||
+ entry.info.category == ApplicationInfo.CATEGORY_GAME;
+ }
+ return !isCategorized;
+ }
+ };
}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
index 8a6ae86..80e1cbf 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
@@ -86,4 +86,25 @@
assertThat(ApplicationsState.FILTER_AUDIO.filterApp(mEntry)).isFalse();
}
+
+ @Test
+ public void testOtherAppsRejectsAudio() {
+ mEntry.info.category = ApplicationInfo.CATEGORY_AUDIO;
+
+ assertThat(ApplicationsState.FILTER_OTHER_APPS.filterApp(mEntry)).isFalse();
+ }
+
+ @Test
+ public void testOtherAppsRejectsGame() {
+ mEntry.info.category = ApplicationInfo.CATEGORY_GAME;
+
+ assertThat(ApplicationsState.FILTER_OTHER_APPS.filterApp(mEntry)).isFalse();
+ }
+
+ @Test
+ public void testOtherAppsAcceptsDefaultCategory() {
+ mEntry.info.category = ApplicationInfo.CATEGORY_UNDEFINED;
+
+ assertThat(ApplicationsState.FILTER_OTHER_APPS.filterApp(mEntry)).isTrue();
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java
index 23eaed9..32b5862 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java
@@ -17,7 +17,6 @@
package com.android.systemui.keyguard;
import static android.app.ActivityManager.TaskDescription;
-import static android.app.ActivityManager.StackId;
import android.annotation.ColorInt;
import android.annotation.UserIdInt;
@@ -32,13 +31,14 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Color;
-import android.graphics.Rect;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.view.View;
+import com.android.internal.annotations.VisibleForTesting;
+
/**
* Bouncer between work activities and the activity used to confirm credentials before unlocking
* a managed profile.
@@ -51,52 +51,39 @@
private static final String TAG = "WorkLockActivity";
/**
- * ID of the locked user that this activity blocks access to.
+ * Contains a {@link TaskDescription} for the activity being covered.
*/
- @UserIdInt
- private int mUserId;
-
+ static final String EXTRA_TASK_DESCRIPTION =
+ "com.android.systemui.keyguard.extra.TASK_DESCRIPTION";
+
/**
- * {@see KeyguardManager}
+ * Cached keyguard manager instance populated by {@link #getKeyguardManager}.
+ * @see KeyguardManager
*/
private KeyguardManager mKgm;
- /**
- * {@see DevicePolicyManager}
- */
- private DevicePolicyManager mDpm;
-
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- mUserId = getIntent().getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId());
- mDpm = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);
- mKgm = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
-
- final IntentFilter lockFilter = new IntentFilter();
- lockFilter.addAction(Intent.ACTION_DEVICE_LOCKED_CHANGED);
- registerReceiverAsUser(mLockEventReceiver, UserHandle.ALL, lockFilter,
- /* permission */ null, /* scheduler */ null);
+ registerReceiverAsUser(mLockEventReceiver, UserHandle.ALL,
+ new IntentFilter(Intent.ACTION_DEVICE_LOCKED_CHANGED), /* permission */ null,
+ /* scheduler */ null);
// Once the receiver is registered, check whether anything happened between now and the time
// when this activity was launched. If it did and the user is unlocked now, just quit.
- if (!mKgm.isDeviceLocked(mUserId)) {
+ if (!getKeyguardManager().isDeviceLocked(getTargetUserId())) {
finish();
return;
}
- // Get the organization color; this is a 24-bit integer provided by a DPC, guaranteed to
- // be completely opaque.
- final @ColorInt int color = mDpm.getOrganizationColorForUser(mUserId);
-
// Draw captions overlaid on the content view, so the whole window is one solid color.
setOverlayWithDecorCaptionEnabled(true);
// Blank out the activity. When it is on-screen it will look like a Recents thumbnail with
// redaction switched on.
final View blankView = new View(this);
- blankView.setBackgroundColor(color);
+ blankView.setBackgroundColor(getPrimaryColor());
setContentView(blankView);
}
@@ -127,26 +114,28 @@
@Override
public void setTaskDescription(TaskDescription taskDescription) {
- // Use the previous activity's task description.
+ // Leave unset so we use the previous activity's task description.
}
private final BroadcastReceiver mLockEventReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, mUserId);
- if (userId == mUserId && !mKgm.isDeviceLocked(mUserId)) {
+ final int targetUserId = getTargetUserId();
+ final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, targetUserId);
+ if (userId == targetUserId && !getKeyguardManager().isDeviceLocked(targetUserId)) {
finish();
}
}
};
private void showConfirmCredentialActivity() {
- if (isFinishing() || !mKgm.isDeviceLocked(mUserId)) {
+ if (isFinishing() || !getKeyguardManager().isDeviceLocked(getTargetUserId())) {
// Don't show the confirm credentials screen if we are already unlocked / unlocking.
return;
}
- final Intent credential = mKgm.createConfirmDeviceCredentialIntent(null, null, mUserId);
+ final Intent credential = getKeyguardManager()
+ .createConfirmDeviceCredentialIntent(null, null, getTargetUserId());
if (credential == null) {
return;
}
@@ -181,4 +170,32 @@
final View view = getWindow().getDecorView();
return ActivityOptions.makeScaleUpAnimation(view, 0, 0, view.getWidth(), view.getHeight());
}
+
+ private KeyguardManager getKeyguardManager() {
+ if (mKgm == null) {
+ mKgm = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
+ }
+ return mKgm;
+ }
+
+ @VisibleForTesting
+ @UserIdInt
+ final int getTargetUserId() {
+ return getIntent().getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId());
+ }
+
+ @VisibleForTesting
+ @ColorInt
+ final int getPrimaryColor() {
+ final TaskDescription taskDescription = (TaskDescription)
+ getIntent().getExtra(EXTRA_TASK_DESCRIPTION);
+ if (taskDescription != null && Color.alpha(taskDescription.getPrimaryColor()) == 255) {
+ return taskDescription.getPrimaryColor();
+ } else {
+ // No task description. Use an organization color set by the policy controller.
+ final DevicePolicyManager devicePolicyManager = (DevicePolicyManager)
+ getSystemService(Context.DEVICE_POLICY_SERVICE);
+ return devicePolicyManager.getOrganizationColorForUser(getTargetUserId());
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java
index e6483f6..a49c482 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java
@@ -31,17 +31,21 @@
public class WorkLockActivityController {
private final Context mContext;
+ final SystemServicesProxy mSsp;
public WorkLockActivityController(Context context) {
mContext = context;
+ mSsp = SystemServicesProxy.getInstance(context);
+
EventBus.getDefault().register(this);
- SystemServicesProxy.getInstance(context).registerTaskStackListener(mLockListener);
+ mSsp.registerTaskStackListener(mLockListener);
}
private void startWorkChallengeInTask(int taskId, int userId) {
Intent intent = new Intent(KeyguardManager.ACTION_CONFIRM_DEVICE_CREDENTIAL_WITH_USER)
.setComponent(new ComponentName(mContext, WorkLockActivity.class))
.putExtra(Intent.EXTRA_USER_ID, userId)
+ .putExtra(WorkLockActivity.EXTRA_TASK_DESCRIPTION, mSsp.getTaskDescription(taskId))
.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
index 49074a6..eae1b81 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
@@ -868,6 +868,14 @@
return null;
}
+ public ActivityManager.TaskDescription getTaskDescription(int taskId) {
+ try {
+ return mIam.getTaskDescription(taskId);
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
/**
* Returns the given icon for a user, badging if necessary.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
index 0160eb7..40aad45 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -20,6 +20,8 @@
import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
@@ -1496,11 +1498,6 @@
// Remove the task from the ignored set
removeIgnoreTask(removedTask);
- // Resize the grid layout task view focus frame
- if (mTaskViewFocusFrame != null) {
- mTaskViewFocusFrame.resize();
- }
-
// If requested, relayout with the given animation
if (animation != null) {
updateLayoutAlgorithm(true /* boundScroll */);
@@ -1838,6 +1835,17 @@
announceForAccessibility(getContext().getString(
R.string.accessibility_recents_item_dismissed, event.task.title));
+ if (useGridLayout() && event.animation != null) {
+ event.animation.setListener(new AnimatorListenerAdapter() {
+ public void onAnimationEnd(Animator animator) {
+ if (mTaskViewFocusFrame != null) {
+ // Resize the grid layout task view focus frame
+ mTaskViewFocusFrame.resize();
+ }
+ }
+ });
+ }
+
// Remove the task from the stack
mStack.removeTask(event.task, event.animation, false /* fromDockGesture */);
EventBus.getDefault().send(new DeleteTaskDataEvent(event.task));
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
index 9a52a7b..a5f7832 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
@@ -270,7 +270,6 @@
return super.onInterceptTouchEvent(ev);
}
-
@Override
protected void measureContents(int width, int height) {
int widthWithoutPadding = width - mPaddingLeft - mPaddingRight;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
index bd5fb92..0ea56b8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
@@ -381,12 +381,7 @@
if (appShortcuts != null) {
result.add(appShortcuts);
}
- synchronized (sLock) {
- // showKeyboardShortcutsDialog only if it has not been dismissed already
- if (sInstance != null) {
- showKeyboardShortcutsDialog(result);
- }
- }
+ showKeyboardShortcutsDialog(result);
}
}, deviceId);
}
@@ -585,7 +580,12 @@
mKeyboardShortcutsDialog.setCanceledOnTouchOutside(true);
Window keyboardShortcutsWindow = mKeyboardShortcutsDialog.getWindow();
keyboardShortcutsWindow.setType(TYPE_SYSTEM_DIALOG);
- mKeyboardShortcutsDialog.show();
+ synchronized (sLock) {
+ // showKeyboardShortcutsDialog only if it has not been dismissed already
+ if (sInstance != null) {
+ mKeyboardShortcutsDialog.show();
+ }
+ }
}
private void populateKeyboardShortcuts(LinearLayout keyboardShortcutsLayout,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.java
new file mode 100644
index 0000000..9b868db
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.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 com.android.systemui.keyguard;
+
+import static android.app.ActivityManager.TaskDescription;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.when;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import android.annotation.ColorInt;
+import android.annotation.UserIdInt;
+import android.app.KeyguardManager;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.os.Looper;
+
+import com.android.systemui.keyguard.WorkLockActivity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * runtest systemui -c com.android.systemui.keyguard.WorkLockActivityTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class WorkLockActivityTest {
+ private static final @UserIdInt int USER_ID = 270;
+ private static final String TASK_LABEL = "task label";
+
+ private @Mock DevicePolicyManager mDevicePolicyManager;
+ private @Mock KeyguardManager mKeyguardManager;
+ private @Mock Context mContext;
+
+ private WorkLockActivity mActivity;
+
+ private static class WorkLockActivityTestable extends WorkLockActivity {
+ WorkLockActivityTestable(Context baseContext) {
+ super();
+ attachBaseContext(baseContext);
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ when(mContext.getSystemService(eq(Context.DEVICE_POLICY_SERVICE)))
+ .thenReturn(mDevicePolicyManager);
+ when(mContext.getSystemService(eq(Context.KEYGUARD_SERVICE)))
+ .thenReturn(mKeyguardManager);
+
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ mActivity = new WorkLockActivityTestable(mContext);
+ }
+
+ @Test
+ public void testBackgroundAlwaysOpaque() throws Exception {
+ final @ColorInt int orgColor = Color.rgb(250, 199, 67);
+ when(mDevicePolicyManager.getOrganizationColorForUser(eq(USER_ID))).thenReturn(orgColor);
+
+ final @ColorInt int opaqueColor= Color.rgb(164, 198, 57);
+ final @ColorInt int transparentColor = Color.argb(0, 0, 0, 0);
+ TaskDescription opaque = new TaskDescription(null, null, opaqueColor);
+ TaskDescription transparent = new TaskDescription(null, null, transparentColor);
+
+ // When a task description is provided with a suitable (opaque) primaryColor, it should be
+ // used as the scrim's background color.
+ mActivity.setIntent(new Intent()
+ .putExtra(Intent.EXTRA_USER_ID, USER_ID)
+ .putExtra(WorkLockActivity.EXTRA_TASK_DESCRIPTION, opaque));
+ assertEquals(opaqueColor, mActivity.getPrimaryColor());
+
+ // When a task description is provided but has no primaryColor / the primaryColor is
+ // transparent, the organization color should be used instead.
+ mActivity.setIntent(new Intent()
+ .putExtra(Intent.EXTRA_USER_ID, USER_ID)
+ .putExtra(WorkLockActivity.EXTRA_TASK_DESCRIPTION, transparent));
+ assertEquals(orgColor, mActivity.getPrimaryColor());
+
+ // When no task description is provided at all, it should be treated like a transparent
+ // description and the organization color shown instead.
+ mActivity.setIntent(new Intent()
+ .putExtra(Intent.EXTRA_USER_ID, USER_ID));
+ assertEquals(orgColor, mActivity.getPrimaryColor());
+ }
+}
diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java
index 6bf0e8d..13e6ae0 100644
--- a/services/backup/java/com/android/server/backup/BackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/BackupManagerService.java
@@ -599,15 +599,42 @@
static final int OP_ACKNOWLEDGED = 1;
static final int OP_TIMEOUT = -1;
- class Operation {
- public int state;
- public BackupRestoreTask callback;
+ private static final int OP_TYPE_WAIT = 0; // Waiting for BackupAgent.
+ private static final int OP_TYPE_BACKUP = 1; // Backup operation in progress.
- Operation(int initialState, BackupRestoreTask callbackObj) {
+ class Operation {
+ int state;
+ final BackupRestoreTask callback;
+ final int type;
+
+ Operation(int initialState, BackupRestoreTask callbackObj, int type) {
state = initialState;
callback = callbackObj;
+ this.type = type;
}
}
+
+ /**
+ * mCurrentOperations contains the list of currently active operations.
+ *
+ * If type of operation is OP_TYPE_WAIT, it are waiting for an ack or timeout.
+ * An operation wraps a BackupRestoreTask within it.
+ * It's the responsibility of this task to remove the operation from this array.
+ *
+ * A BackupRestore task gets notified of ack/timeout for the operation via
+ * BackupRestoreTask#handleCancel, BackupRestoreTask#operationComplete and notifyAll called
+ * on the mCurrentOpLock. {@link BackupManagerService#waitUntilOperationComplete(int)} is
+ * used in various places to 'wait' for notifyAll and detect change of pending state of an
+ * operation. So typically, an operation will be removed from this array by:
+ * - BackupRestoreTask#handleCancel and
+ * - BackupRestoreTask#operationComplete OR waitUntilOperationComplete. Do not remove at both
+ * these places because waitUntilOperationComplete relies on the operation being present to
+ * determine its completion status.
+ *
+ * If type of operation is OP_BACKUP, it is a task running backups. It provides a handle to
+ * cancel backup tasks.
+ */
+ @GuardedBy("mCurrentOpLock")
final SparseArray<Operation> mCurrentOperations = new SparseArray<Operation>();
final Object mCurrentOpLock = new Object();
final Random mTokenGenerator = new Random();
@@ -967,7 +994,8 @@
case MSG_TIMEOUT:
{
- handleTimeout(msg.arg1, msg.obj);
+ Slog.d(TAG, "Timeout message received for token=" + Integer.toHexString(msg.arg1));
+ handleCancel(msg.arg1, false);
break;
}
@@ -2364,6 +2392,33 @@
return BackupManager.SUCCESS;
}
+ // Cancel all running backups.
+ public void cancelBackups(){
+ mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "cancelBackups");
+ if (MORE_DEBUG) {
+ Slog.i(TAG, "cancelBackups() called.");
+ }
+ final long oldToken = Binder.clearCallingIdentity();
+ try {
+ synchronized (mCurrentOpLock) {
+ for (int i = 0; i < mCurrentOperations.size(); i++) {
+ Operation op = mCurrentOperations.valueAt(i);
+ int token = mCurrentOperations.keyAt(i);
+ if (op.type == OP_TYPE_BACKUP) {
+ handleCancel(token, true /* cancelAll */);
+ }
+ }
+ }
+
+ // We don't want the backup jobs to kick in any time soon.
+ // Reschedules them to run in the distant future.
+ KeyValueBackupJob.schedule(mContext, BUSY_BACKOFF_MIN_MILLIS);
+ FullBackupJob.schedule(mContext, 2 * BUSY_BACKOFF_MIN_MILLIS);
+ } finally {
+ Binder.restoreCallingIdentity(oldToken);
+ }
+ }
+
// -----
// Interface and methods used by the asynchronous-with-timeout backup/restore operations
@@ -2375,20 +2430,34 @@
void operationComplete(long result);
// An operation that wanted a callback has timed out
- void handleTimeout();
+ void handleCancel(boolean cancelAll);
}
- void prepareOperationTimeout(int token, long interval, BackupRestoreTask callback) {
+ void prepareOperationTimeout(int token, long interval, BackupRestoreTask callback,
+ int operationType) {
if (MORE_DEBUG) Slog.v(TAG, "starting timeout: token=" + Integer.toHexString(token)
+ " interval=" + interval + " callback=" + callback);
synchronized (mCurrentOpLock) {
- mCurrentOperations.put(token, new Operation(OP_PENDING, callback));
+ mCurrentOperations.put(token, new Operation(OP_PENDING, callback, operationType));
Message msg = mBackupHandler.obtainMessage(MSG_TIMEOUT, token, 0, callback);
mBackupHandler.sendMessageDelayed(msg, interval);
}
}
+ private void removeOperation(int token) {
+ if (MORE_DEBUG) {
+ Slog.d(TAG, "Removing operation token=" + Integer.toHexString(token));
+ }
+ synchronized (mCurrentOpLock) {
+ if (mCurrentOperations.get(token) == null) {
+ Slog.w(TAG, "Duplicate remove for operation. token=" +
+ Integer.toHexString(token));
+ }
+ mCurrentOperations.remove(token);
+ }
+ }
+
// synchronous waiter case
boolean waitUntilOperationComplete(int token) {
if (MORE_DEBUG) Slog.i(TAG, "Blocking until operation complete for "
@@ -2405,9 +2474,14 @@
if (op.state == OP_PENDING) {
try {
mCurrentOpLock.wait();
- } catch (InterruptedException e) {}
+ } catch (InterruptedException e) {
+ }
// When the wait is notified we loop around and recheck the current state
} else {
+ if (MORE_DEBUG) {
+ Slog.d(TAG, "Unblocked waiting for operation token=" +
+ Integer.toHexString(token));
+ }
// No longer pending; we're done
finalState = op.state;
break;
@@ -2416,33 +2490,36 @@
}
}
+ removeOperation(token);
mBackupHandler.removeMessages(MSG_TIMEOUT);
if (MORE_DEBUG) Slog.v(TAG, "operation " + Integer.toHexString(token)
+ " complete: finalState=" + finalState);
return finalState == OP_ACKNOWLEDGED;
}
- void handleTimeout(int token, Object obj) {
+ void handleCancel(int token, boolean cancelAll) {
// Notify any synchronous waiters
Operation op = null;
synchronized (mCurrentOpLock) {
op = mCurrentOperations.get(token);
if (MORE_DEBUG) {
- if (op == null) Slog.w(TAG, "Timeout of token " + Integer.toHexString(token)
+ if (op == null) Slog.w(TAG, "Cancel of token " + Integer.toHexString(token)
+ " but no op found");
}
int state = (op != null) ? op.state : OP_TIMEOUT;
if (state == OP_ACKNOWLEDGED) {
// The operation finished cleanly, so we have nothing more to do.
- if (MORE_DEBUG) {
- Slog.v(TAG, "handleTimeout() after success; cleanup happens now");
+ if (DEBUG) {
+ Slog.w(TAG, "Operation already got an ack." +
+ "Should have been removed from mCurrentOperations.");
}
op = null;
mCurrentOperations.delete(token);
} else if (state == OP_PENDING) {
- if (DEBUG) Slog.v(TAG, "TIMEOUT: token=" + Integer.toHexString(token));
+ if (DEBUG) Slog.v(TAG, "Cancel: token=" + Integer.toHexString(token));
op.state = OP_TIMEOUT;
- // Leaves the object in place for later ack
+ // Can't delete op from mCurrentOperations here. waitUntilOperationComplete may be
+ // called after we receive cancel here. We need this op's state there.
}
mCurrentOpLock.notifyAll();
}
@@ -2450,9 +2527,9 @@
// If there's a TimeoutHandler for this event, call it
if (op != null && op.callback != null) {
if (MORE_DEBUG) {
- Slog.v(TAG, " Invoking timeout on " + op.callback);
+ Slog.v(TAG, " Invoking cancel on " + op.callback);
}
- op.callback.handleTimeout();
+ op.callback.handleCancel(cancelAll);
}
}
@@ -2464,9 +2541,36 @@
FINAL
}
+ /**
+ * This class handles the process of backing up a given list of key/value backup packages.
+ * Also takes in a list of pending dolly backups and kicks them off when key/value backups
+ * are done.
+ *
+ * Flow:
+ * If required, backup @pm@.
+ * For each pending key/value backup package:
+ * - Bind to agent.
+ * - Call agent.doBackup()
+ * - Wait either for cancel/timeout or operationComplete() callback from the agent.
+ * Start task to perform dolly backups.
+ *
+ * There are three entry points into this class:
+ * - execute() [Called from the handler thread]
+ * - operationComplete(long result) [Called from the handler thread]
+ * - handleCancel(boolean cancelAll) [Can be called from any thread]
+ * These methods synchronize on mCancelLock.
+ *
+ * Interaction with mCurrentOperations:
+ * - An entry for this task is put into mCurrentOperations for the entire lifetime of the
+ * task. This is useful to cancel the task if required.
+ * - An ephemeral entry is put into mCurrentOperations each time we are waiting on for
+ * response from a backup agent. This is used to plumb timeouts and completion callbacks.
+ */
class PerformBackupTask implements BackupRestoreTask {
private static final String TAG = "PerformBackupTask";
+ private final Object mCancelLock = new Object();
+
IBackupTransport mTransport;
ArrayList<BackupRequest> mQueue;
ArrayList<BackupRequest> mOriginalQueue;
@@ -2477,6 +2581,10 @@
IBackupObserver mObserver;
IBackupManagerMonitor mMonitor;
+ private final PerformFullTransportBackupTask mFullBackupTask;
+ private final int mCurrentOpToken;
+ private volatile int mEphemeralOpToken;
+
// carried information about the current in-flight operation
IBackupAgent mAgentBinder;
PackageInfo mCurrentPackage;
@@ -2491,6 +2599,8 @@
final boolean mUserInitiated;
final boolean mNonIncremental;
+ private volatile boolean mCancelAll;
+
public PerformBackupTask(IBackupTransport transport, String dirName,
ArrayList<BackupRequest> queue, File journal, IBackupObserver observer,
IBackupManagerMonitor monitor, ArrayList<String> pendingFullBackups,
@@ -2505,33 +2615,63 @@
mNonIncremental = nonIncremental;
mStateDir = new File(mBaseStateDir, dirName);
+ mCurrentOpToken = generateToken();
mCurrentState = BackupState.INITIAL;
mFinished = false;
+ CountDownLatch latch = new CountDownLatch(1);
+ String[] fullBackups =
+ mPendingFullBackups.toArray(new String[mPendingFullBackups.size()]);
+ mFullBackupTask =
+ new PerformFullTransportBackupTask(/*fullBackupRestoreObserver*/ null,
+ fullBackups, /*updateSchedule*/ false, /*runningJob*/ null, latch,
+ mObserver, mMonitor,mUserInitiated);
+
+ registerTask();
addBackupTrace("STATE => INITIAL");
}
+ /**
+ * Put this task in the repository of running tasks.
+ */
+ private void registerTask() {
+ synchronized (mCurrentOpLock) {
+ mCurrentOperations.put(mCurrentOpToken, new Operation(OP_PENDING, this,
+ OP_TYPE_BACKUP));
+ }
+ }
+
+ /**
+ * Remove this task from repository of running tasks.
+ */
+ private void unregisterTask() {
+ removeOperation(mCurrentOpToken);
+ }
+
// Main entry point: perform one chunk of work, updating the state as appropriate
// and reposting the next chunk to the primary backup handler thread.
@Override
+ @GuardedBy("mCancelLock")
public void execute() {
- switch (mCurrentState) {
- case INITIAL:
- beginBackup();
- break;
+ synchronized (mCancelLock) {
+ switch (mCurrentState) {
+ case INITIAL:
+ beginBackup();
+ break;
- case RUNNING_QUEUE:
- invokeNextAgent();
- break;
+ case RUNNING_QUEUE:
+ invokeNextAgent();
+ break;
- case FINAL:
- if (!mFinished) finalizeBackup();
- else {
- Slog.e(TAG, "Duplicate finish");
- }
- mFinished = true;
- break;
+ case FINAL:
+ if (!mFinished) finalizeBackup();
+ else {
+ Slog.e(TAG, "Duplicate finish");
+ }
+ mFinished = true;
+ break;
+ }
}
}
@@ -2796,6 +2936,12 @@
void finalizeBackup() {
addBackupTrace("finishing");
+ // Mark packages that we didn't backup (because backup was cancelled, etc.) as needing
+ // backup.
+ for (BackupRequest req : mQueue) {
+ dataChangedImpl(req.packageName);
+ }
+
// Either backup was successful, in which case we of course do not need
// this pass's journal any more; or it failed, in which case we just
// re-enqueued all of these packages in the current active journal.
@@ -2850,7 +2996,9 @@
clearBackupTrace();
- if (mStatus == BackupTransport.TRANSPORT_OK &&
+ unregisterTask();
+
+ if (!mCancelAll && mStatus == BackupTransport.TRANSPORT_OK &&
mPendingFullBackups != null && !mPendingFullBackups.isEmpty()) {
Slog.d(TAG, "Starting full backups for: " + mPendingFullBackups);
CountDownLatch latch = new CountDownLatch(1);
@@ -2862,8 +3010,12 @@
mObserver, mMonitor, mUserInitiated);
// Acquiring wakelock for PerformFullTransportBackupTask before its start.
mWakelock.acquire();
- (new Thread(task, "full-transport-requested")).start();
+ (new Thread(mFullBackupTask, "full-transport-requested")).start();
+ } else if (mCancelAll) {
+ mFullBackupTask.unregisterTask();
+ sendBackupFinished(mObserver, BackupManager.ERROR_BACKUP_CANCELLED);
} else {
+ mFullBackupTask.unregisterTask();
switch (mStatus) {
case BackupTransport.TRANSPORT_OK:
sendBackupFinished(mObserver, BackupManager.SUCCESS);
@@ -2905,8 +3057,8 @@
mBackupData = null;
mNewState = null;
- final int token = generateToken();
boolean callingAgent = false;
+ mEphemeralOpToken = generateToken();
try {
// Look up the package info & signatures. This is first so that if it
// throws an exception, there's no file setup yet that would need to
@@ -2945,10 +3097,11 @@
// Initiate the target's backup pass
addBackupTrace("setting timeout");
- prepareOperationTimeout(token, TIMEOUT_BACKUP_INTERVAL, this);
+ prepareOperationTimeout(mEphemeralOpToken, TIMEOUT_BACKUP_INTERVAL, this,
+ OP_TYPE_WAIT);
addBackupTrace("calling agent doBackup()");
- agent.doBackup(mSavedState, mBackupData, mNewState, quota, token,
+ agent.doBackup(mSavedState, mBackupData, mNewState, quota, mEphemeralOpToken,
mBackupManagerBinder);
} catch (Exception e) {
Slog.e(TAG, "Error invoking for backup on " + packageName + ". " + e);
@@ -3060,195 +3213,225 @@
}
@Override
+ @GuardedBy("mCancelLock")
public void operationComplete(long unusedResult) {
- // The agent reported back to us!
-
- if (mBackupData == null) {
- // This callback was racing with our timeout, so we've cleaned up the
- // agent state already and are on to the next thing. We have nothing
- // further to do here: agent state having been cleared means that we've
- // initiated the appropriate next operation.
- final String pkg = (mCurrentPackage != null)
- ? mCurrentPackage.packageName : "[none]";
- if (MORE_DEBUG) {
- Slog.i(TAG, "Callback after agent teardown: " + pkg);
+ removeOperation(mEphemeralOpToken);
+ synchronized (mCancelLock) {
+ // The agent reported back to us!
+ if (mFinished) {
+ Slog.d(TAG, "operationComplete received after task finished.");
+ return;
}
- addBackupTrace("late opComplete; curPkg = " + pkg);
- return;
- }
- final String pkgName = mCurrentPackage.packageName;
- final long filepos = mBackupDataName.length();
- FileDescriptor fd = mBackupData.getFileDescriptor();
- try {
- // If it's a 3rd party app, see whether they wrote any protected keys
- // and complain mightily if they are attempting shenanigans.
- if (mCurrentPackage.applicationInfo != null &&
- (mCurrentPackage.applicationInfo.flags&ApplicationInfo.FLAG_SYSTEM) == 0) {
- ParcelFileDescriptor readFd = ParcelFileDescriptor.open(mBackupDataName,
- ParcelFileDescriptor.MODE_READ_ONLY);
- BackupDataInput in = new BackupDataInput(readFd.getFileDescriptor());
- try {
- while (in.readNextHeader()) {
- final String key = in.getKey();
- if (key != null && key.charAt(0) >= 0xff00) {
- // Not okay: crash them and bail.
- failAgent(mAgentBinder, "Illegal backup key: " + key);
- addBackupTrace("illegal key " + key + " from " + pkgName);
- EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, pkgName,
- "bad key");
- mBackupHandler.removeMessages(MSG_TIMEOUT);
- sendBackupOnPackageResult(mObserver, pkgName,
- BackupManager.ERROR_AGENT_FAILURE);
- errorCleanup();
- // errorCleanup() implicitly executes next state properly
- return;
- }
- in.skipEntityData();
- }
- } finally {
- if (readFd != null) {
- readFd.close();
- }
+ if (mBackupData == null) {
+ // This callback was racing with our timeout, so we've cleaned up the
+ // agent state already and are on to the next thing. We have nothing
+ // further to do here: agent state having been cleared means that we've
+ // initiated the appropriate next operation.
+ final String pkg = (mCurrentPackage != null)
+ ? mCurrentPackage.packageName : "[none]";
+ if (MORE_DEBUG) {
+ Slog.i(TAG, "Callback after agent teardown: " + pkg);
}
+ addBackupTrace("late opComplete; curPkg = " + pkg);
+ return;
}
- // Piggyback the widget state payload, if any
- writeWidgetPayloadIfAppropriate(fd, pkgName);
- } catch (IOException e) {
- // Hard disk error; recovery/failure policy TBD. For now roll back,
- // but we may want to consider this a transport-level failure (i.e.
- // we're in such a bad state that we can't contemplate doing backup
- // operations any more during this pass).
- Slog.w(TAG, "Unable to save widget state for " + pkgName);
+ final String pkgName = mCurrentPackage.packageName;
+ final long filepos = mBackupDataName.length();
+ FileDescriptor fd = mBackupData.getFileDescriptor();
try {
- Os.ftruncate(fd, filepos);
- } catch (ErrnoException ee) {
- Slog.w(TAG, "Unable to roll back!");
- }
- }
-
- // Spin the data off to the transport and proceed with the next stage.
- if (MORE_DEBUG) Slog.v(TAG, "operationComplete(): sending data to transport for "
- + pkgName);
- mBackupHandler.removeMessages(MSG_TIMEOUT);
- clearAgentState();
- addBackupTrace("operation complete");
-
- ParcelFileDescriptor backupData = null;
- mStatus = BackupTransport.TRANSPORT_OK;
- long size = 0;
- try {
- size = mBackupDataName.length();
- if (size > 0) {
- if (mStatus == BackupTransport.TRANSPORT_OK) {
- backupData = ParcelFileDescriptor.open(mBackupDataName,
+ // If it's a 3rd party app, see whether they wrote any protected keys
+ // and complain mightily if they are attempting shenanigans.
+ if (mCurrentPackage.applicationInfo != null &&
+ (mCurrentPackage.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM)
+ == 0) {
+ ParcelFileDescriptor readFd = ParcelFileDescriptor.open(mBackupDataName,
ParcelFileDescriptor.MODE_READ_ONLY);
- addBackupTrace("sending data to transport");
- int flags = mUserInitiated ? BackupTransport.FLAG_USER_INITIATED : 0;
- mStatus = mTransport.performBackup(mCurrentPackage, backupData, flags);
+ BackupDataInput in = new BackupDataInput(readFd.getFileDescriptor());
+ try {
+ while (in.readNextHeader()) {
+ final String key = in.getKey();
+ if (key != null && key.charAt(0) >= 0xff00) {
+ // Not okay: crash them and bail.
+ failAgent(mAgentBinder, "Illegal backup key: " + key);
+ addBackupTrace("illegal key " + key + " from " + pkgName);
+ EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, pkgName,
+ "bad key");
+ mBackupHandler.removeMessages(MSG_TIMEOUT);
+ sendBackupOnPackageResult(mObserver, pkgName,
+ BackupManager.ERROR_AGENT_FAILURE);
+ errorCleanup();
+ // agentErrorCleanup() implicitly executes next state properly
+ return;
+ }
+ in.skipEntityData();
+ }
+ } finally {
+ if (readFd != null) {
+ readFd.close();
+ }
+ }
}
- // TODO - We call finishBackup() for each application backed up, because
- // we need to know now whether it succeeded or failed. Instead, we should
- // hold off on finishBackup() until the end, which implies holding off on
- // renaming *all* the output state files (see below) until that happens.
-
- addBackupTrace("data delivered: " + mStatus);
- if (mStatus == BackupTransport.TRANSPORT_OK) {
- addBackupTrace("finishing op on transport");
- mStatus = mTransport.finishBackup();
- addBackupTrace("finished: " + mStatus);
- } else if (mStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) {
- addBackupTrace("transport rejected package");
+ // Piggyback the widget state payload, if any
+ writeWidgetPayloadIfAppropriate(fd, pkgName);
+ } catch (IOException e) {
+ // Hard disk error; recovery/failure policy TBD. For now roll back,
+ // but we may want to consider this a transport-level failure (i.e.
+ // we're in such a bad state that we can't contemplate doing backup
+ // operations any more during this pass).
+ Slog.w(TAG, "Unable to save widget state for " + pkgName);
+ try {
+ Os.ftruncate(fd, filepos);
+ } catch (ErrnoException ee) {
+ Slog.w(TAG, "Unable to roll back!");
}
- } else {
- if (MORE_DEBUG) Slog.i(TAG, "no backup data written; not calling transport");
- addBackupTrace("no data to send");
}
- if (mStatus == BackupTransport.TRANSPORT_OK) {
- // After successful transport, delete the now-stale data
- // and juggle the files so that next time we supply the agent
- // with the new state file it just created.
- mBackupDataName.delete();
- mNewStateName.renameTo(mSavedStateName);
- sendBackupOnPackageResult(mObserver, pkgName, BackupManager.SUCCESS);
- EventLog.writeEvent(EventLogTags.BACKUP_PACKAGE, pkgName, size);
- logBackupComplete(pkgName);
- } else if (mStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) {
- // The transport has rejected backup of this specific package. Roll it
- // back but proceed with running the rest of the queue.
- mBackupDataName.delete();
- mNewStateName.delete();
- sendBackupOnPackageResult(mObserver, pkgName,
- BackupManager.ERROR_TRANSPORT_PACKAGE_REJECTED);
- EventLogTags.writeBackupAgentFailure(pkgName, "Transport rejected");
- } else if (mStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
- sendBackupOnPackageResult(mObserver, pkgName,
- BackupManager.ERROR_TRANSPORT_QUOTA_EXCEEDED);
- EventLog.writeEvent(EventLogTags.BACKUP_QUOTA_EXCEEDED, pkgName);
- } else {
- // Actual transport-level failure to communicate the data to the backend
+ // Spin the data off to the transport and proceed with the next stage.
+ if (MORE_DEBUG) Slog.v(TAG, "operationComplete(): sending data to transport for "
+ + pkgName);
+ mBackupHandler.removeMessages(MSG_TIMEOUT);
+ clearAgentState();
+ addBackupTrace("operation complete");
+
+ ParcelFileDescriptor backupData = null;
+ mStatus = BackupTransport.TRANSPORT_OK;
+ long size = 0;
+ try {
+ size = mBackupDataName.length();
+ if (size > 0) {
+ if (mStatus == BackupTransport.TRANSPORT_OK) {
+ backupData = ParcelFileDescriptor.open(mBackupDataName,
+ ParcelFileDescriptor.MODE_READ_ONLY);
+ addBackupTrace("sending data to transport");
+ int flags = mUserInitiated ? BackupTransport.FLAG_USER_INITIATED : 0;
+ mStatus = mTransport.performBackup(mCurrentPackage, backupData, flags);
+ }
+
+ // TODO - We call finishBackup() for each application backed up, because
+ // we need to know now whether it succeeded or failed. Instead, we should
+ // hold off on finishBackup() until the end, which implies holding off on
+ // renaming *all* the output state files (see below) until that happens.
+
+ addBackupTrace("data delivered: " + mStatus);
+ if (mStatus == BackupTransport.TRANSPORT_OK) {
+ addBackupTrace("finishing op on transport");
+ mStatus = mTransport.finishBackup();
+ addBackupTrace("finished: " + mStatus);
+ } else if (mStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) {
+ addBackupTrace("transport rejected package");
+ }
+ } else {
+ if (MORE_DEBUG) Slog.i(TAG,
+ "no backup data written; not calling transport");
+ addBackupTrace("no data to send");
+ }
+
+ if (mStatus == BackupTransport.TRANSPORT_OK) {
+ // After successful transport, delete the now-stale data
+ // and juggle the files so that next time we supply the agent
+ // with the new state file it just created.
+ mBackupDataName.delete();
+ mNewStateName.renameTo(mSavedStateName);
+ sendBackupOnPackageResult(mObserver, pkgName, BackupManager.SUCCESS);
+ EventLog.writeEvent(EventLogTags.BACKUP_PACKAGE, pkgName, size);
+ logBackupComplete(pkgName);
+ } else if (mStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) {
+ // The transport has rejected backup of this specific package. Roll it
+ // back but proceed with running the rest of the queue.
+ mBackupDataName.delete();
+ mNewStateName.delete();
+ sendBackupOnPackageResult(mObserver, pkgName,
+ BackupManager.ERROR_TRANSPORT_PACKAGE_REJECTED);
+ EventLogTags.writeBackupAgentFailure(pkgName, "Transport rejected");
+ } else if (mStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
+ sendBackupOnPackageResult(mObserver, pkgName,
+ BackupManager.ERROR_TRANSPORT_QUOTA_EXCEEDED);
+ EventLog.writeEvent(EventLogTags.BACKUP_QUOTA_EXCEEDED, pkgName);
+ } else {
+ // Actual transport-level failure to communicate the data to the backend
+ sendBackupOnPackageResult(mObserver, pkgName,
+ BackupManager.ERROR_TRANSPORT_ABORTED);
+ EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, pkgName);
+ }
+ } catch (Exception e) {
sendBackupOnPackageResult(mObserver, pkgName,
BackupManager.ERROR_TRANSPORT_ABORTED);
+ Slog.e(TAG, "Transport error backing up " + pkgName, e);
EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, pkgName);
- }
- } catch (Exception e) {
- sendBackupOnPackageResult(mObserver, pkgName,
- BackupManager.ERROR_TRANSPORT_ABORTED);
- Slog.e(TAG, "Transport error backing up " + pkgName, e);
- EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, pkgName);
- mStatus = BackupTransport.TRANSPORT_ERROR;
- } finally {
- try { if (backupData != null) backupData.close(); } catch (IOException e) {}
- }
-
- final BackupState nextState;
- if (mStatus == BackupTransport.TRANSPORT_OK
- || mStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) {
- // Success or single-package rejection. Proceed with the next app if any,
- // otherwise we're done.
- nextState = (mQueue.isEmpty()) ? BackupState.FINAL : BackupState.RUNNING_QUEUE;
- } else if (mStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
- if (MORE_DEBUG) {
- Slog.d(TAG, "Package " + mCurrentPackage.packageName +
- " hit quota limit on k/v backup");
- }
- if (mAgentBinder != null) {
+ mStatus = BackupTransport.TRANSPORT_ERROR;
+ } finally {
try {
- long quota = mTransport.getBackupQuota(mCurrentPackage.packageName, false);
- mAgentBinder.doQuotaExceeded(size, quota);
- } catch (Exception e) {
- Slog.e(TAG, "Unable to notify about quota exceeded: " + e.getMessage());
+ if (backupData != null) backupData.close();
+ } catch (IOException e) {
}
}
- nextState = (mQueue.isEmpty()) ? BackupState.FINAL : BackupState.RUNNING_QUEUE;
- } else {
- // Any other error here indicates a transport-level failure. That means
- // we need to halt everything and reschedule everything for next time.
- revertAndEndBackup();
- nextState = BackupState.FINAL;
- }
- executeNextState(nextState);
+ final BackupState nextState;
+ if (mStatus == BackupTransport.TRANSPORT_OK
+ || mStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) {
+ // Success or single-package rejection. Proceed with the next app if any,
+ // otherwise we're done.
+ nextState = (mQueue.isEmpty()) ? BackupState.FINAL : BackupState.RUNNING_QUEUE;
+ } else if (mStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
+ if (MORE_DEBUG) {
+ Slog.d(TAG, "Package " + mCurrentPackage.packageName +
+ " hit quota limit on k/v backup");
+ }
+ if (mAgentBinder != null) {
+ try {
+ long quota = mTransport.getBackupQuota(mCurrentPackage.packageName,
+ false);
+ mAgentBinder.doQuotaExceeded(size, quota);
+ } catch (Exception e) {
+ Slog.e(TAG, "Unable to notify about quota exceeded: " + e.getMessage());
+ }
+ }
+ nextState = (mQueue.isEmpty()) ? BackupState.FINAL : BackupState.RUNNING_QUEUE;
+ } else {
+ // Any other error here indicates a transport-level failure. That means
+ // we need to halt everything and reschedule everything for next time.
+ revertAndEndBackup();
+ nextState = BackupState.FINAL;
+ }
+
+ executeNextState(nextState);
+ }
}
+
@Override
- public void handleTimeout() {
- // Whoops, the current agent timed out running doBackup(). Tidy up and restage
- // it for the next time we run a backup pass.
- // !!! TODO: keep track of failure counts per agent, and blacklist those which
- // fail repeatedly (i.e. have proved themselves to be buggy).
- Slog.e(TAG, "Timeout backing up " + mCurrentPackage.packageName);
- EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, mCurrentPackage.packageName,
- "timeout");
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_TIMEOUT,
- mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT);
- addBackupTrace("timeout of " + mCurrentPackage.packageName);
- errorCleanup();
- dataChangedImpl(mCurrentPackage.packageName);
+ @GuardedBy("mCancelLock")
+ public void handleCancel(boolean cancelAll) {
+ removeOperation(mEphemeralOpToken);
+ synchronized (mCancelLock) {
+ if (mFinished) {
+ // We have already cancelled this operation.
+ if (MORE_DEBUG) {
+ Slog.d(TAG, "Ignoring stale cancel. cancelAll=" + cancelAll);
+ }
+ return;
+ }
+ mCancelAll = cancelAll;
+ // Whoops, the current agent timed out running doBackup(). Tidy up and restage
+ // it for the next time we run a backup pass.
+ // !!! TODO: keep track of failure counts per agent, and blacklist those which
+ // fail repeatedly (i.e. have proved themselves to be buggy).
+ Slog.e(TAG, "Cancel backing up " + mCurrentPackage.packageName);
+ EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, mCurrentPackage.packageName);
+ addBackupTrace(
+ "cancel of " + mCurrentPackage.packageName + ", cancelAll=" + cancelAll);
+ errorCleanup();
+ if (!cancelAll) {
+ executeNextState(
+ mQueue.isEmpty() ? BackupState.FINAL : BackupState.RUNNING_QUEUE);
+ dataChangedImpl(mCurrentPackage.packageName);
+ } else {
+ finalizeBackup();
+ }
+ }
}
void revertAndEndBackup() {
@@ -3276,8 +3459,6 @@
mBackupDataName.delete();
mNewStateName.delete();
clearAgentState();
-
- executeNextState(mQueue.isEmpty() ? BackupState.FINAL : BackupState.RUNNING_QUEUE);
}
// Cleanup common to both success and failure cases
@@ -3289,7 +3470,7 @@
// Current-operation callback handling requires the validity of these various
// bits of internal state as an invariant of the operation still being live.
// This means we make sure to clear all of the state in unison inside the lock.
- mCurrentOperations.clear();
+ mCurrentOperations.remove(mEphemeralOpToken);
mSavedState = mBackupData = mNewState = null;
}
@@ -3343,7 +3524,7 @@
try {
pipes = ParcelFileDescriptor.createPipe();
int token = generateToken();
- prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL, null);
+ prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL, null, OP_TYPE_WAIT);
mService.backupObbs(pkg.packageName, pipes[1], token, mBackupManagerBinder);
routeSocketDataToOutput(pipes[0], out);
success = waitUntilOperationComplete(token);
@@ -3481,6 +3662,7 @@
boolean mIncludeApks;
PackageInfo mPkg;
private final long mQuota;
+ private final int mOpToken;
class FullBackupRunner implements Runnable {
PackageInfo mPackage;
@@ -3535,7 +3717,7 @@
if (DEBUG) Slog.d(TAG, "Calling doFullBackup() on " + mPackage.packageName);
prepareOperationTimeout(mToken, TIMEOUT_FULL_BACKUP_INTERVAL,
- mTimeoutMonitor /* in parent class */);
+ mTimeoutMonitor /* in parent class */, OP_TYPE_WAIT);
mAgent.doFullBackup(mPipe, mQuota, mToken, mBackupManagerBinder);
} catch (IOException e) {
Slog.e(TAG, "Error running full backup for " + mPackage.packageName);
@@ -3551,7 +3733,7 @@
}
FullBackupEngine(OutputStream output, FullBackupPreflight preflightHook, PackageInfo pkg,
- boolean alsoApks, BackupRestoreTask timeoutMonitor, long quota) {
+ boolean alsoApks, BackupRestoreTask timeoutMonitor, long quota, int opToken) {
mOutput = output;
mPreflightHook = preflightHook;
mPkg = pkg;
@@ -3561,6 +3743,7 @@
mManifestFile = new File(mFilesDir, BACKUP_MANIFEST_FILENAME);
mMetadataFile = new File(mFilesDir, BACKUP_METADATA_FILENAME);
mQuota = quota;
+ mOpToken = opToken;
}
public int preflightCheck() throws RemoteException {
@@ -3603,9 +3786,8 @@
byte[] widgetBlob = AppWidgetBackupBridge.getWidgetState(mPkg.packageName,
UserHandle.USER_SYSTEM);
- final int token = generateToken();
FullBackupRunner runner = new FullBackupRunner(mPkg, mAgent, pipes[1],
- token, sendApk, !isSharedStorage, widgetBlob);
+ mOpToken, sendApk, !isSharedStorage, widgetBlob);
pipes[1].close(); // the runner has dup'd it
pipes[1] = null;
Thread t = new Thread(runner, "app-data-runner");
@@ -3614,7 +3796,7 @@
// Now pull data from the app and stuff it into the output
routeSocketDataToOutput(pipes[0], mOutput);
- if (!waitUntilOperationComplete(token)) {
+ if (!waitUntilOperationComplete(mOpToken)) {
Slog.e(TAG, "Full backup failed on package " + mPkg.packageName);
} else {
if (MORE_DEBUG) {
@@ -3865,12 +4047,14 @@
PackageInfo mCurrentTarget;
String mCurrentPassword;
String mEncryptPassword;
+ private final int mCurrentOpToken;
PerformAdbBackupTask(ParcelFileDescriptor fd, IFullBackupRestoreObserver observer,
boolean includeApks, boolean includeObbs, boolean includeShared,
boolean doWidgets, String curPassword, String encryptPassword, boolean doAllApps,
boolean doSystem, boolean doCompress, String[] packages, AtomicBoolean latch) {
super(observer);
+ mCurrentOpToken = generateToken();
mLatch = latch;
mOutputFile = fd;
@@ -4156,8 +4340,7 @@
final boolean isSharedStorage =
pkg.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE);
- mBackupEngine = new FullBackupEngine(out, null, pkg, mIncludeApks,
- this /* BackupRestoreTask */, Long.MAX_VALUE /* quota */);
+ mBackupEngine = new FullBackupEngine(out, null, pkg, mIncludeApks, this, Long.MAX_VALUE, mCurrentOpToken);
sendOnBackupPackage(isSharedStorage ? "Shared storage" : pkg.packageName);
// Don't need to check preflight result as there is no preflight hook.
@@ -4190,9 +4373,6 @@
} catch (IOException e) {
/* nothing we can do about this */
}
- synchronized (mCurrentOpLock) {
- mCurrentOperations.clear();
- }
synchronized (mLatch) {
mLatch.set(true);
mLatch.notifyAll();
@@ -4216,29 +4396,68 @@
}
@Override
- public void handleTimeout() {
+ public void handleCancel(boolean cancelAll) {
final PackageInfo target = mCurrentTarget;
if (DEBUG) {
- Slog.w(TAG, "adb backup timeout of " + target);
+ Slog.w(TAG, "adb backup cancel of " + target);
}
if (target != null) {
tearDownAgentAndKill(mCurrentTarget.applicationInfo);
}
+ removeOperation(mCurrentOpToken);
}
}
- // Full backup task extension used for transport-oriented operation
- class PerformFullTransportBackupTask extends FullBackupTask {
+ /**
+ * Full backup task extension used for transport-oriented operation.
+ *
+ * Flow:
+ * For each requested package:
+ * - Spin off a new SinglePackageBackupRunner (mBackupRunner) for the current package.
+ * - Wait until preflight is complete. (mBackupRunner.getPreflightResultBlocking())
+ * - If preflight data size is within limit, start reading data from agent pipe and writing
+ * to transport pipe. While there is data to send, call transport.sendBackupData(int) to
+ * tell the transport how many bytes to expect on its pipe.
+ * - After sending all data, call transport.finishBackup() if things went well. And
+ * transport.cancelFullBackup() otherwise.
+ *
+ * Interactions with mCurrentOperations:
+ * - An entry for this object is added to mCurrentOperations for the entire lifetime of this
+ * object. Used to cancel the operation.
+ * - SinglePackageBackupRunner and SinglePackageBackupPreflight will put ephemeral entries
+ * to get timeouts or operation complete callbacks.
+ *
+ * Handling cancels:
+ * - The contract we provide is that the task won't interact with the transport after
+ * handleCancel() is done executing.
+ * - This task blocks at 3 points: 1. Preflight result check 2. Reading on agent side pipe
+ * and 3. Get backup result from mBackupRunner.
+ * - Bubbling up handleCancel to mBackupRunner handles all 3: 1. Calls handleCancel on the
+ * preflight operation which counts down on the preflight latch. 2. Tears down the agent,
+ * so read() returns -1. 3. Notifies mCurrentOpLock which unblocks
+ * mBackupRunner.getBackupResultBlocking().
+ */
+ class PerformFullTransportBackupTask extends FullBackupTask implements BackupRestoreTask {
static final String TAG = "PFTBT";
+
+ private final Object mCancelLock = new Object();
+
ArrayList<PackageInfo> mPackages;
PackageInfo mCurrentPackage;
boolean mUpdateSchedule;
CountDownLatch mLatch;
- AtomicBoolean mKeepRunning; // signal from job scheduler
FullBackupJob mJob; // if a scheduled job needs to be finished afterwards
IBackupObserver mBackupObserver;
IBackupManagerMonitor mMonitor;
boolean mUserInitiated;
+ private volatile IBackupTransport mTransport;
+ SinglePackageBackupRunner mBackupRunner;
+ private final int mBackupRunnerOpToken;
+
+ // This is true when a backup operation for some package is in progress.
+ private volatile boolean mIsDoingBackup;
+ private volatile boolean mCancelAll;
+ private final int mCurrentOpToken;
PerformFullTransportBackupTask(IFullBackupRestoreObserver observer,
String[] whichPackages, boolean updateSchedule,
@@ -4247,12 +4466,15 @@
super(observer);
mUpdateSchedule = updateSchedule;
mLatch = latch;
- mKeepRunning = new AtomicBoolean(true);
mJob = runningJob;
mPackages = new ArrayList<PackageInfo>(whichPackages.length);
mBackupObserver = backupObserver;
mMonitor = monitor;
mUserInitiated = userInitiated;
+ mCurrentOpToken = generateToken();
+ mBackupRunnerOpToken = generateToken();
+
+ registerTask();
for (String pkg : whichPackages) {
try {
@@ -4298,12 +4520,60 @@
}
}
- public void setRunning(boolean running) {
- mKeepRunning.set(running);
+ private void registerTask() {
+ synchronized (mCurrentOpLock) {
+ Slog.d(TAG, "backupmanager pftbt token=" + Integer.toHexString(mCurrentOpToken));
+ mCurrentOperations.put(mCurrentOpToken, new Operation(OP_PENDING, this,
+ OP_TYPE_BACKUP));
+ }
+ }
+
+ private void unregisterTask() {
+ removeOperation(mCurrentOpToken);
+ }
+
+ @Override
+ public void execute() {
+ // Nothing to do.
+ }
+
+ @Override
+ public void handleCancel(boolean cancelAll) {
+ synchronized (mCancelLock) {
+ // We only support 'cancelAll = true' case for this task. Cancelling of a single package
+
+ // due to timeout is handled by SinglePackageBackupRunner and SinglePackageBackupPreflight.
+
+ if (!cancelAll) {
+ Slog.wtf(TAG, "Expected cancelAll to be true.");
+ }
+
+ if (mCancelAll) {
+ Slog.d(TAG, "Ignoring duplicate cancel call.");
+ return;
+ }
+
+ mCancelAll = true;
+ if (mIsDoingBackup) {
+ BackupManagerService.this.handleCancel(mBackupRunnerOpToken, cancelAll);
+ try {
+ mTransport.cancelFullBackup();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Error calling cancelFullBackup() on transport: " + e);
+ // Can't do much.
+ }
+ }
+ }
+ }
+
+ @Override
+ public void operationComplete(long result) {
+ // Nothing to do.
}
@Override
public void run() {
+
// data from the app, passed to us for bridging to the transport
ParcelFileDescriptor[] enginePipes = null;
@@ -4325,8 +4595,8 @@
return;
}
- IBackupTransport transport = mTransportManager.getCurrentTransportBinder();
- if (transport == null) {
+ mTransport = mTransportManager.getCurrentTransportBinder();
+ if (mTransport == null) {
Slog.w(TAG, "Transport not present; full data backup not performed");
backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
return;
@@ -4347,30 +4617,40 @@
// Tell the transport the data's coming
int flags = mUserInitiated ? BackupTransport.FLAG_USER_INITIATED : 0;
- int backupPackageStatus = transport.performFullBackup(currentPackage,
- transportPipes[0], flags);
+ int backupPackageStatus;
+ long quota = Long.MAX_VALUE;
+ synchronized (mCancelLock) {
+ if (mCancelAll) {
+ break;
+ }
+ backupPackageStatus = mTransport.performFullBackup(currentPackage,
+ transportPipes[0], flags);
+
+ if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {
+ quota = mTransport.getBackupQuota(currentPackage.packageName,
+ true /* isFullBackup */);
+ // Now set up the backup engine / data source end of things
+ enginePipes = ParcelFileDescriptor.createPipe();
+ mBackupRunner =
+ new SinglePackageBackupRunner(enginePipes[1], currentPackage,
+ mTransport, quota, mBackupRunnerOpToken);
+ // The runner dup'd the pipe half, so we close it here
+ enginePipes[1].close();
+ enginePipes[1] = null;
+
+ mIsDoingBackup = true;
+ }
+ }
if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {
- final long quota = transport.getBackupQuota(currentPackage.packageName,
- true /* isFullBackup */);
// The transport has its own copy of the read end of the pipe,
// so close ours now
transportPipes[0].close();
transportPipes[0] = null;
- // Now set up the backup engine / data source end of things
- enginePipes = ParcelFileDescriptor.createPipe();
-
- SinglePackageBackupRunner backupRunner =
- new SinglePackageBackupRunner(enginePipes[1], currentPackage,
- transport, quota);
- // The runner dup'd the pipe half, so we close it here
- enginePipes[1].close();
- enginePipes[1] = null;
-
// Spin off the runner to fetch the app's data and pipe it
// into the engine pipes
- (new Thread(backupRunner, "package-backup-bridge")).start();
+ (new Thread(mBackupRunner, "package-backup-bridge")).start();
// Read data off the engine pipe and pass it to the transport
// pipe until we hit EOD on the input stream. We do not take
@@ -4380,7 +4660,7 @@
FileOutputStream out = new FileOutputStream(
transportPipes[1].getFileDescriptor());
long totalRead = 0;
- final long preflightResult = backupRunner.getPreflightResultBlocking();
+ final long preflightResult = mBackupRunner.getPreflightResultBlocking();
// Preflight result is negative if some error happened on preflight.
if (preflightResult < 0) {
if (MORE_DEBUG) {
@@ -4392,19 +4672,17 @@
} else {
int nRead = 0;
do {
- if (!mKeepRunning.get()) {
- if (DEBUG_SCHEDULING) {
- Slog.i(TAG, "Full backup task told to stop");
- }
- break;
- }
nRead = in.read(buffer);
if (MORE_DEBUG) {
Slog.v(TAG, "in.read(buffer) from app: " + nRead);
}
if (nRead > 0) {
out.write(buffer, 0, nRead);
- backupPackageStatus = transport.sendBackupData(nRead);
+ synchronized (mCancelLock) {
+ if (!mCancelAll) {
+ backupPackageStatus = mTransport.sendBackupData(nRead);
+ }
+ }
totalRead += nRead;
if (mBackupObserver != null && preflightResult > 0) {
sendBackupOnUpdate(mBackupObserver, packageName,
@@ -4413,29 +4691,32 @@
}
} while (nRead > 0
&& backupPackageStatus == BackupTransport.TRANSPORT_OK);
-
// Despite preflight succeeded, package still can hit quota on flight.
if (backupPackageStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
Slog.w(TAG, "Package hit quota limit in-flight " + packageName
+ ": " + totalRead + " of " + quota);
- backupRunner.sendQuotaExceeded(totalRead, quota);
+ mBackupRunner.sendQuotaExceeded(totalRead, quota);
}
}
- // If we've lost our running criteria, tell the transport to cancel
- // and roll back this (partial) backup payload; otherwise tell it
- // that we've reached the clean finish state.
- if (!mKeepRunning.get()) {
- backupPackageStatus = BackupTransport.TRANSPORT_ERROR;
- transport.cancelFullBackup();
- } else {
- // If we were otherwise in a good state, now interpret the final
- // result based on what finishBackup() returns. If we're in a
- // failure case already, preserve that result and ignore whatever
- // finishBackup() reports.
- final int finishResult = transport.finishBackup();
- if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {
- backupPackageStatus = finishResult;
+ final int backupRunnerResult = mBackupRunner.getBackupResultBlocking();
+
+ synchronized (mCancelLock) {
+ mIsDoingBackup = false;
+ // If mCancelCurrent is true, we have already called cancelFullBackup().
+ if (!mCancelAll) {
+ if (backupRunnerResult == BackupTransport.TRANSPORT_OK) {
+ // If we were otherwise in a good state, now interpret the final
+ // result based on what finishBackup() returns. If we're in a
+ // failure case already, preserve that result and ignore whatever
+ // finishBackup() reports.
+ final int finishResult = mTransport.finishBackup();
+ if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {
+ backupPackageStatus = finishResult;
+ }
+ } else {
+ mTransport.cancelFullBackup();
+ }
}
}
@@ -4449,8 +4730,7 @@
// errors take precedence over agent/app-specific errors for purposes of
// determining our course of action.
if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {
- // We still could fail in backup runner thread, getting result from there.
- int backupRunnerResult = backupRunner.getBackupResultBlocking();
+ // We still could fail in backup runner thread.
if (backupRunnerResult != BackupTransport.TRANSPORT_OK) {
// If there was an error in runner thread and
// not TRANSPORT_ERROR here, overwrite it.
@@ -4474,7 +4754,7 @@
// Also ask the transport how long it wants us to wait before
// moving on to the next package, if any.
- backoff = transport.requestFullBackupTime();
+ backoff = mTransport.requestFullBackupTime();
if (DEBUG_SCHEDULING) {
Slog.i(TAG, "Transport suggested backoff=" + backoff);
}
@@ -4513,6 +4793,14 @@
EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName);
tearDownAgentAndKill(currentPackage.applicationInfo);
// Do nothing, clean up, and continue looping.
+ } else if (backupPackageStatus == BackupManager.ERROR_BACKUP_CANCELLED) {
+ sendBackupOnPackageResult(mBackupObserver, packageName,
+ BackupManager.ERROR_BACKUP_CANCELLED);
+ Slog.w(TAG, "Backup cancelled. package=" + packageName +
+ ", cancelAll=" + mCancelAll);
+ EventLog.writeEvent(EventLogTags.FULL_BACKUP_CANCELLED, packageName);
+ tearDownAgentAndKill(currentPackage.applicationInfo);
+ // Do nothing, clean up, and continue looping.
} else if (backupPackageStatus != BackupTransport.TRANSPORT_OK) {
sendBackupOnPackageResult(mBackupObserver, packageName,
BackupManager.ERROR_TRANSPORT_ABORTED);
@@ -4542,6 +4830,11 @@
backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
Slog.w(TAG, "Exception trying full transport backup", e);
} finally {
+
+ if (mCancelAll) {
+ backupRunStatus = BackupManager.ERROR_BACKUP_CANCELLED;
+ }
+
if (DEBUG) {
Slog.i(TAG, "Full backup completed with status: " + backupRunStatus);
}
@@ -4550,6 +4843,8 @@
cleanUpPipes(transportPipes);
cleanUpPipes(enginePipes);
+ unregisterTask();
+
if (mJob != null) {
mJob.finishBackupPass();
}
@@ -4565,6 +4860,7 @@
if (mUpdateSchedule) {
scheduleNextFullBackupJob(backoff);
}
+
Slog.i(BackupManagerService.TAG, "Full data backup pass finished.");
mWakelock.release();
}
@@ -4601,23 +4897,24 @@
final CountDownLatch mLatch = new CountDownLatch(1);
final IBackupTransport mTransport;
final long mQuota;
+ private final int mCurrentOpToken;
- public SinglePackageBackupPreflight(IBackupTransport transport, long quota) {
+ SinglePackageBackupPreflight(IBackupTransport transport, long quota, int currentOpToken) {
mTransport = transport;
mQuota = quota;
+ mCurrentOpToken = currentOpToken;
}
@Override
public int preflightFullBackup(PackageInfo pkg, IBackupAgent agent) {
int result;
try {
- final int token = generateToken();
- prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL, this);
+ prepareOperationTimeout(mCurrentOpToken, TIMEOUT_FULL_BACKUP_INTERVAL, this, OP_TYPE_WAIT);
addBackupTrace("preflighting");
if (MORE_DEBUG) {
Slog.d(TAG, "Preflighting full payload of " + pkg.packageName);
}
- agent.doMeasureFullBackup(mQuota, token, mBackupManagerBinder);
+ agent.doMeasureFullBackup(mQuota, mCurrentOpToken, mBackupManagerBinder);
// Now wait to get our result back. If this backstop timeout is reached without
// the latch being thrown, flow will continue as though a result or "normal"
@@ -4652,7 +4949,7 @@
@Override
public void execute() {
- // Unused in this case
+ // Unused.
}
@Override
@@ -4663,15 +4960,17 @@
}
mResult.set(result);
mLatch.countDown();
+ removeOperation(mCurrentOpToken);
}
@Override
- public void handleTimeout() {
+ public void handleCancel(boolean cancelAll) {
if (MORE_DEBUG) {
- Slog.i(TAG, "Preflight timeout; failing");
+ Slog.i(TAG, "Preflight cancelled; failing");
}
mResult.set(BackupTransport.AGENT_ERROR);
mLatch.countDown();
+ removeOperation(mCurrentOpToken);
}
@Override
@@ -4688,43 +4987,67 @@
class SinglePackageBackupRunner implements Runnable, BackupRestoreTask {
final ParcelFileDescriptor mOutput;
final PackageInfo mTarget;
- final FullBackupPreflight mPreflight;
+ final SinglePackageBackupPreflight mPreflight;
final CountDownLatch mPreflightLatch;
final CountDownLatch mBackupLatch;
+ private final int mCurrentOpToken;
+ private final int mEphemeralToken;
private FullBackupEngine mEngine;
private volatile int mPreflightResult;
private volatile int mBackupResult;
private final long mQuota;
+ private volatile boolean mIsCancelled;
SinglePackageBackupRunner(ParcelFileDescriptor output, PackageInfo target,
- IBackupTransport transport, long quota) throws IOException {
+ IBackupTransport transport, long quota, int currentOpToken) throws IOException {
mOutput = ParcelFileDescriptor.dup(output.getFileDescriptor());
mTarget = target;
- mPreflight = new SinglePackageBackupPreflight(transport, quota);
+ mCurrentOpToken = currentOpToken;
+ mEphemeralToken = generateToken();
+ mPreflight = new SinglePackageBackupPreflight(transport, quota, mEphemeralToken);
mPreflightLatch = new CountDownLatch(1);
mBackupLatch = new CountDownLatch(1);
mPreflightResult = BackupTransport.AGENT_ERROR;
mBackupResult = BackupTransport.AGENT_ERROR;
mQuota = quota;
+ registerTask();
+ }
+
+ void registerTask() {
+ synchronized (mCurrentOpLock) {
+ mCurrentOperations.put(mCurrentOpToken, new Operation(OP_PENDING, this,
+ OP_TYPE_WAIT));
+ }
+ }
+
+ void unregisterTask() {
+ synchronized (mCurrentOpLock) {
+ mCurrentOperations.remove(mCurrentOpToken);
+ }
}
@Override
public void run() {
FileOutputStream out = new FileOutputStream(mOutput.getFileDescriptor());
- mEngine = new FullBackupEngine(out, mPreflight, mTarget, false, this, mQuota);
+ mEngine = new FullBackupEngine(out, mPreflight, mTarget, false, this, mQuota, mCurrentOpToken);
try {
try {
- mPreflightResult = mEngine.preflightCheck();
+ if (!mIsCancelled) {
+ mPreflightResult = mEngine.preflightCheck();
+ }
} finally {
mPreflightLatch.countDown();
}
// If there is no error on preflight, continue backup.
if (mPreflightResult == BackupTransport.TRANSPORT_OK) {
- mBackupResult = mEngine.backupOnePackage();
+ if (!mIsCancelled) {
+ mBackupResult = mEngine.backupOnePackage();
+ }
}
} catch (Exception e) {
Slog.e(TAG, "Exception during full package backup of " + mTarget.packageName);
} finally {
+ unregisterTask();
mBackupLatch.countDown();
try {
mOutput.close();
@@ -4743,6 +5066,9 @@
long getPreflightResultBlocking() {
try {
mPreflightLatch.await(TIMEOUT_FULL_BACKUP_INTERVAL, TimeUnit.MILLISECONDS);
+ if (mIsCancelled) {
+ return BackupManager.ERROR_BACKUP_CANCELLED;
+ }
if (mPreflightResult == BackupTransport.TRANSPORT_OK) {
return mPreflight.getExpectedSizeOrErrorCode();
} else {
@@ -4756,6 +5082,9 @@
int getBackupResultBlocking() {
try {
mBackupLatch.await(TIMEOUT_FULL_BACKUP_INTERVAL, TimeUnit.MILLISECONDS);
+ if (mIsCancelled) {
+ return BackupManager.ERROR_BACKUP_CANCELLED;
+ }
return mBackupResult;
} catch (InterruptedException e) {
return BackupTransport.AGENT_ERROR;
@@ -4772,14 +5101,23 @@
public void operationComplete(long result) { /* intentionally empty */ }
@Override
- public void handleTimeout() {
+ public void handleCancel(boolean cancelAll) {
if (DEBUG) {
- Slog.w(TAG, "Full backup timeout of " + mTarget.packageName);
+ Slog.w(TAG, "Full backup cancel of " + mTarget.packageName);
}
+
mMonitor = monitorEvent(mMonitor,
BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_TIMEOUT,
mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT);
+ mIsCancelled = true;
+ // Cancel tasks spun off by this task.
+ BackupManagerService.this.handleCancel(mEphemeralToken, cancelAll);
tearDownAgentAndKill(mTarget.applicationInfo);
+ // Free up everyone waiting on this task and its children.
+ mPreflightLatch.countDown();
+ mBackupLatch.countDown();
+ // We are done with this operation.
+ removeOperation(mCurrentOpToken);
}
}
}
@@ -5057,7 +5395,7 @@
if (DEBUG_SCHEDULING) {
Slog.i(TAG, "Telling running backup to stop");
}
- mRunningFullBackupTask.setRunning(false);
+ mRunningFullBackupTask.handleCancel(true);
}
}
}
@@ -5191,6 +5529,8 @@
// Widget blob to be restored out-of-band
byte[] mWidgetData = null;
+ private final int mEphemeralOpToken;
+
// Runner that can be placed in a separate thread to do in-process
// invocations of the full restore API asynchronously. Used by adb restore.
class RestoreFileRunnable implements Runnable {
@@ -5226,7 +5566,9 @@
}
public FullRestoreEngine(BackupRestoreTask monitorTask, IFullBackupRestoreObserver observer,
- PackageInfo onlyPackage, boolean allowApks, boolean allowObbs) {
+ PackageInfo onlyPackage, boolean allowApks, boolean allowObbs,
+ int ephemeralOpToken) {
+ mEphemeralOpToken = ephemeralOpToken;
mMonitorTask = monitorTask;
mObserver = observer;
mOnlyPackage = onlyPackage;
@@ -5431,17 +5773,16 @@
if (okay) {
boolean agentSuccess = true;
long toCopy = info.size;
- final int token = generateToken();
try {
- prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL,
- mMonitorTask);
+ prepareOperationTimeout(mEphemeralOpToken, TIMEOUT_FULL_BACKUP_INTERVAL,
+ mMonitorTask, OP_TYPE_WAIT);
if (info.domain.equals(FullBackup.OBB_TREE_TOKEN)) {
if (DEBUG) Slog.d(TAG, "Restoring OBB file for " + pkg
+ " : " + info.path);
mObbConnection.restoreObbFile(pkg, mPipes[0],
info.size, info.type, info.path, info.mode,
- info.mtime, token, mBackupManagerBinder);
+ info.mtime, mEphemeralOpToken, mBackupManagerBinder);
} else {
if (MORE_DEBUG) Slog.d(TAG, "Invoking agent to restore file "
+ info.path);
@@ -5452,12 +5793,12 @@
if (mTargetApp.processName.equals("system")) {
Slog.d(TAG, "system process agent - spinning a thread");
RestoreFileRunnable runner = new RestoreFileRunnable(
- mAgent, info, mPipes[0], token);
+ mAgent, info, mPipes[0], mEphemeralOpToken);
new Thread(runner, "restore-sys-runner").start();
} else {
mAgent.doRestoreFile(mPipes[0], info.size, info.type,
info.domain, info.path, info.mode, info.mtime,
- token, mBackupManagerBinder);
+ mEphemeralOpToken, mBackupManagerBinder);
}
}
} catch (IOException e) {
@@ -5509,7 +5850,7 @@
// and now that we've sent it all, wait for the remote
// side to acknowledge receipt
- agentSuccess = waitUntilOperationComplete(token);
+ agentSuccess = waitUntilOperationComplete(mEphemeralOpToken);
}
// okay, if the remote end failed at any point, deal with
@@ -6354,9 +6695,11 @@
class AdbRestoreFinishedLatch implements BackupRestoreTask {
static final String TAG = "AdbRestoreFinishedLatch";
final CountDownLatch mLatch;
+ private final int mCurrentOpToken;
- AdbRestoreFinishedLatch() {
+ AdbRestoreFinishedLatch(int currentOpToken) {
mLatch = new CountDownLatch(1);
+ mCurrentOpToken = currentOpToken;
}
void await() {
@@ -6379,14 +6722,16 @@
Slog.w(TAG, "adb onRestoreFinished() complete");
}
mLatch.countDown();
+ removeOperation(mCurrentOpToken);
}
@Override
- public void handleTimeout() {
+ public void handleCancel(boolean cancelAll) {
if (DEBUG) {
Slog.w(TAG, "adb onRestoreFinished() timed out");
}
mLatch.countDown();
+ removeOperation(mCurrentOpToken);
}
}
@@ -6577,9 +6922,6 @@
Slog.w(TAG, "Close of restore data pipe threw", e);
/* nothing we can do about this */
}
- synchronized (mCurrentOpLock) {
- mCurrentOperations.clear();
- }
synchronized (mLatchObject) {
mLatchObject.set(true);
mLatchObject.notifyAll();
@@ -6868,7 +7210,8 @@
long toCopy = info.size;
final int token = generateToken();
try {
- prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL, null);
+ prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL, null,
+ OP_TYPE_WAIT);
if (info.domain.equals(FullBackup.OBB_TREE_TOKEN)) {
if (DEBUG) Slog.d(TAG, "Restoring OBB file for " + pkg
+ " : " + info.path);
@@ -7003,8 +7346,9 @@
// In the adb restore case, we do restore-finished here
if (doRestoreFinished) {
final int token = generateToken();
- final AdbRestoreFinishedLatch latch = new AdbRestoreFinishedLatch();
- prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL, latch);
+ final AdbRestoreFinishedLatch latch = new AdbRestoreFinishedLatch(token);
+ prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL, latch,
+ OP_TYPE_WAIT);
if (mTargetApp.processName.equals("system")) {
if (MORE_DEBUG) {
Slog.d(TAG, "system agent - restoreFinished on thread");
@@ -7872,11 +8216,14 @@
ParcelFileDescriptor mBackupData;
ParcelFileDescriptor mNewState;
+ private final int mEphemeralOpToken;
+
// Invariant: mWakelock is already held, and this task is responsible for
// releasing it at the end of the restore operation.
PerformUnifiedRestoreTask(IBackupTransport transport, IRestoreObserver observer,
IBackupManagerMonitor monitor, long restoreSetToken, PackageInfo targetPackage,
int pmToken, boolean isFullSystemRestore, String[] filterSet) {
+ mEphemeralOpToken = generateToken();
mState = UnifiedRestoreState.INITIAL;
mStartRealtime = SystemClock.elapsedRealtime();
@@ -8304,7 +8651,6 @@
ParcelFileDescriptor stage;
File downloadFile = (staging) ? mStageName : mBackupDataName;
- final int token = generateToken();
try {
// Run the transport's restore pass
stage = ParcelFileDescriptor.open(downloadFile,
@@ -8377,9 +8723,9 @@
// Kick off the restore, checking for hung agents. The timeout or
// the operationComplete() callback will schedule the next step,
// so we do not do that here.
- prepareOperationTimeout(token, TIMEOUT_RESTORE_INTERVAL, this);
+ prepareOperationTimeout(mEphemeralOpToken, TIMEOUT_RESTORE_INTERVAL, this, OP_TYPE_WAIT);
mAgent.doRestore(mBackupData, appVersionCode, mNewState,
- token, mBackupManagerBinder);
+ mEphemeralOpToken, mBackupManagerBinder);
} catch (Exception e) {
Slog.e(TAG, "Unable to call app for restore: " + packageName, e);
EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
@@ -8426,9 +8772,9 @@
// state RESTORE_FINISHED : provide the "no more data" signpost callback at the end
private void restoreFinished() {
try {
- final int token = generateToken();
- prepareOperationTimeout(token, TIMEOUT_RESTORE_FINISHED_INTERVAL, this);
- mAgent.doRestoreFinished(token, mBackupManagerBinder);
+ prepareOperationTimeout(mEphemeralOpToken, TIMEOUT_RESTORE_FINISHED_INTERVAL, this,
+ OP_TYPE_WAIT);
+ mAgent.doRestoreFinished(mEphemeralOpToken, mBackupManagerBinder);
// If we get this far, the callback or timeout will schedule the
// next restore state, so we're done
} catch (Exception e) {
@@ -8452,7 +8798,10 @@
// pipe through which the engine will read data. [0] read, [1] write
ParcelFileDescriptor[] mEnginePipes;
+ private final int mEphemeralOpToken;
+
public StreamFeederThread() throws IOException {
+ mEphemeralOpToken = generateToken();
mTransportPipes = ParcelFileDescriptor.createPipe();
mEnginePipes = ParcelFileDescriptor.createPipe();
setRunning(true);
@@ -8466,7 +8815,7 @@
EventLog.writeEvent(EventLogTags.FULL_RESTORE_PACKAGE,
mCurrentPackage.packageName);
- mEngine = new FullRestoreEngine(this, null, mCurrentPackage, false, false);
+ mEngine = new FullRestoreEngine(this, null, mCurrentPackage, false, false, mEphemeralOpToken);
mEngineThread = new EngineThread(mEngine, mEnginePipes[0]);
ParcelFileDescriptor eWriteEnd = mEnginePipes[1];
@@ -8605,7 +8954,8 @@
// The app has timed out handling a restoring file
@Override
- public void handleTimeout() {
+ public void handleCancel(boolean cancelAll) {
+ removeOperation(mEphemeralOpToken);
if (DEBUG) {
Slog.w(TAG, "Full-data restore target timed out; shutting down");
}
@@ -8785,13 +9135,11 @@
// The caller is responsible for reestablishing the state machine; our
// responsibility here is to clear the decks for whatever comes next.
mBackupHandler.removeMessages(MSG_TIMEOUT, this);
- synchronized (mCurrentOpLock) {
- mCurrentOperations.clear();
- }
}
@Override
public void operationComplete(long unusedResult) {
+ removeOperation(mEphemeralOpToken);
if (MORE_DEBUG) {
Slog.i(TAG, "operationComplete() during restore: target="
+ mCurrentPackage.packageName
@@ -8852,7 +9200,8 @@
// A call to agent.doRestore() or agent.doRestoreFinished() has timed out
@Override
- public void handleTimeout() {
+ public void handleCancel(boolean cancelAll) {
+ removeOperation(mEphemeralOpToken);
Slog.e(TAG, "Timeout restoring application " + mCurrentPackage.packageName);
mMonitor = monitorEvent(mMonitor, BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_RESTORE_TIMEOUT,
mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT);
@@ -9966,7 +10315,16 @@
// and ignore it; we've already dealt with the timeout.
op = null;
mCurrentOperations.delete(token);
- } else {
+ } else if (op.state == OP_ACKNOWLEDGED) {
+ if (DEBUG) {
+ Slog.w(TAG, "Received duplicate ack for token=" +
+ Integer.toHexString(token));
+ }
+ op = null;
+ mCurrentOperations.remove(token);
+ } else if (op.state == OP_PENDING) {
+ // Can't delete op from mCurrentOperations. waitUntilOperationComplete can be
+ // called after we we receive this call.
op.state = OP_ACKNOWLEDGED;
}
}
diff --git a/services/backup/java/com/android/server/backup/Trampoline.java b/services/backup/java/com/android/server/backup/Trampoline.java
index da0cee5..8855661 100644
--- a/services/backup/java/com/android/server/backup/Trampoline.java
+++ b/services/backup/java/com/android/server/backup/Trampoline.java
@@ -363,6 +363,14 @@
}
@Override
+ public void cancelBackups() throws RemoteException {
+ BackupManagerService svc = mService;
+ if (svc != null) {
+ svc.cancelBackups();
+ }
+ }
+
+ @Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags
index 4559254..2e61550 100644
--- a/services/core/java/com/android/server/EventLogTags.logtags
+++ b/services/core/java/com/android/server/EventLogTags.logtags
@@ -132,6 +132,7 @@
2843 full_backup_success (Package|3)
2844 full_restore_package (Package|3)
2845 full_backup_quota_exceeded (Package|3)
+2846 full_backup_cancelled (Package|3),(Message|3)
2850 backup_transport_lifecycle (Transport|3),(Bound|1|1)
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 072b0ea..a73eb18 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -9623,6 +9623,20 @@
}
@Override
+ public ActivityManager.TaskDescription getTaskDescription(int id) {
+ synchronized (this) {
+ enforceCallingPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS,
+ "getTaskDescription()");
+ final TaskRecord tr = mStackSupervisor.anyTaskForIdLocked(
+ id, !RESTORE_FROM_RECENTS, INVALID_STACK_ID);
+ if (tr != null) {
+ return tr.lastTaskDescription;
+ }
+ }
+ return null;
+ }
+
+ @Override
public int addAppTask(IBinder activityToken, Intent intent,
ActivityManager.TaskDescription description, Bitmap thumbnail) throws RemoteException {
final int callingUid = Binder.getCallingUid();
diff --git a/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java b/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
index 37221a9..5e51579 100644
--- a/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
+++ b/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
@@ -250,31 +250,33 @@
}
private void cleanupUpstream() {
- if (mMyUpstreamIfaceName != null) {
- // note that we don't care about errors here.
- // sometimes interfaces are gone before we get
- // to remove their rules, which generates errors.
- // just do the best we can.
- try {
- // about to tear down NAT; gather remaining statistics
- mStatsService.forceUpdate();
- } catch (Exception e) {
- if (VDBG) Log.e(TAG, "Exception in forceUpdate: " + e.toString());
- }
- try {
- mNMService.stopInterfaceForwarding(mIfaceName, mMyUpstreamIfaceName);
- } catch (Exception e) {
- if (VDBG) Log.e(
- TAG, "Exception in removeInterfaceForward: " + e.toString());
- }
- try {
- mNMService.disableNat(mIfaceName, mMyUpstreamIfaceName);
- } catch (Exception e) {
- if (VDBG) Log.e(TAG, "Exception in disableNat: " + e.toString());
- }
- mMyUpstreamIfaceName = null;
+ if (mMyUpstreamIfaceName == null) return;
+
+ cleanupUpstreamInterface(mMyUpstreamIfaceName);
+ mMyUpstreamIfaceName = null;
+ }
+
+ private void cleanupUpstreamInterface(String upstreamIface) {
+ // Note that we don't care about errors here.
+ // Sometimes interfaces are gone before we get
+ // to remove their rules, which generates errors.
+ // Just do the best we can.
+ try {
+ // About to tear down NAT; gather remaining statistics.
+ mStatsService.forceUpdate();
+ } catch (Exception e) {
+ if (VDBG) Log.e(TAG, "Exception in forceUpdate: " + e.toString());
}
- return;
+ try {
+ mNMService.stopInterfaceForwarding(mIfaceName, upstreamIface);
+ } catch (Exception e) {
+ if (VDBG) Log.e(TAG, "Exception in removeInterfaceForward: " + e.toString());
+ }
+ try {
+ mNMService.disableNat(mIfaceName, upstreamIface);
+ } catch (Exception e) {
+ if (VDBG) Log.e(TAG, "Exception in disableNat: " + e.toString());
+ }
}
@Override
@@ -306,6 +308,7 @@
newUpstreamIfaceName);
} catch (Exception e) {
Log.e(TAG, "Exception enabling Nat: " + e.toString());
+ cleanupUpstreamInterface(newUpstreamIfaceName);
mLastError = ConnectivityManager.TETHER_ERROR_ENABLE_NAT_ERROR;
transitionTo(mInitialState);
return true;
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 19c9d9b..771ae9a 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -2458,12 +2458,14 @@
Slog.w(TAG, "getBackupPayload: cannot backup policy for user " + user);
return null;
}
- final ByteArrayOutputStream baos = new ByteArrayOutputStream();
- try {
- writePolicyXml(baos, true /*forBackup*/);
- return baos.toByteArray();
- } catch (IOException e) {
- Slog.w(TAG, "getBackupPayload: error writing payload for user " + user, e);
+ synchronized(mPolicyFile) {
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ writePolicyXml(baos, true /*forBackup*/);
+ return baos.toByteArray();
+ } catch (IOException e) {
+ Slog.w(TAG, "getBackupPayload: error writing payload for user " + user, e);
+ }
}
return null;
}
@@ -2481,12 +2483,14 @@
Slog.w(TAG, "applyRestore: cannot restore policy for user " + user);
return;
}
- final ByteArrayInputStream bais = new ByteArrayInputStream(payload);
- try {
- readPolicyXml(bais, true /*forRestore*/);
- savePolicyFile();
- } catch (NumberFormatException | XmlPullParserException | IOException e) {
- Slog.w(TAG, "applyRestore: error reading payload", e);
+ synchronized(mPolicyFile) {
+ final ByteArrayInputStream bais = new ByteArrayInputStream(payload);
+ try {
+ readPolicyXml(bais, true /*forRestore*/);
+ savePolicyFile();
+ } catch (NumberFormatException | XmlPullParserException | IOException e) {
+ Slog.w(TAG, "applyRestore: error reading payload", e);
+ }
}
}
diff --git a/services/core/java/com/android/server/pm/SELinuxMMAC.java b/services/core/java/com/android/server/pm/SELinuxMMAC.java
index 2781150..7e7de21 100644
--- a/services/core/java/com/android/server/pm/SELinuxMMAC.java
+++ b/services/core/java/com/android/server/pm/SELinuxMMAC.java
@@ -72,6 +72,9 @@
// Append ephemeral to existing seinfo label
private static final String EPHEMERAL_APP_STR = ":ephemeralapp";
+ // Append targetSdkVersion=n to existing seinfo label where n is the app's targetSdkVersion
+ private static final String TARGETSDKVERSION_STR = ":targetSdkVersion=";
+
/**
* Load the mac_permissions.xml file containing all seinfo assignments used to
* label apps. The loaded mac_permissions.xml file is determined by the
@@ -296,6 +299,8 @@
if (pkg.applicationInfo.isPrivilegedApp())
pkg.applicationInfo.seinfo += PRIVILEGED_APP_STR;
+ pkg.applicationInfo.seinfo += TARGETSDKVERSION_STR + pkg.applicationInfo.targetSdkVersion;
+
if (DEBUG_POLICY_INSTALL) {
Slog.i(TAG, "package (" + pkg.packageName + ") labeled with " +
"seinfo=" + pkg.applicationInfo.seinfo);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 69d436c..fb28bcf 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -99,6 +99,7 @@
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
+import android.content.res.Resources;
import android.database.ContentObserver;
import android.graphics.Bitmap;
import android.graphics.Color;
@@ -208,7 +209,7 @@
*/
public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
- private static final String LOG_TAG = "DevicePolicyManager";
+ protected static final String LOG_TAG = "DevicePolicyManager";
private static final boolean VERBOSE_LOG = false; // DO NOT SUBMIT WITH TRUE
@@ -252,7 +253,6 @@
private static final String ACTION_EXPIRED_PASSWORD_NOTIFICATION
= "com.android.server.ACTION_EXPIRED_PASSWORD_NOTIFICATION";
- private static final int MONITORING_CERT_NOTIFICATION_ID = R.plurals.ssl_ca_cert_warning;
private static final int PROFILE_WIPED_NOTIFICATION_ID = 1001;
private static final int NETWORK_LOGGING_NOTIFICATION_ID = 1002;
@@ -409,6 +409,7 @@
}
};
+ /** Listens only if mHasFeature == true. */
private final BroadcastReceiver mRemoteBugreportFinishedReceiver = new BroadcastReceiver() {
@Override
@@ -420,6 +421,7 @@
}
};
+ /** Listens only if mHasFeature == true. */
private final BroadcastReceiver mRemoteBugreportConsentReceiver = new BroadcastReceiver() {
@Override
@@ -513,7 +515,21 @@
final Handler mHandler;
- BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ /** Listens on any device, even when mHasFeature == false. */
+ final BroadcastReceiver mRootCaReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (StorageManager.inCryptKeeperBounce()) {
+ return;
+ }
+ final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, getSendingUserId());
+ new MonitoringCertNotificationTask(DevicePolicyManagerService.this, mInjector)
+ .execute(userHandle);
+ }
+ };
+
+ /** Listens only if mHasFeature == true. */
+ final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
@@ -559,14 +575,7 @@
}
});
}
- if (Intent.ACTION_USER_UNLOCKED.equals(action)
- || Intent.ACTION_USER_STARTED.equals(action)
- || KeyChain.ACTION_TRUST_STORE_CHANGED.equals(action)) {
- if (!StorageManager.inCryptKeeperBounce()) {
- new MonitoringCertNotificationTask().execute(
- intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_ALL));
- }
- }
+
if (Intent.ACTION_USER_ADDED.equals(action)) {
sendUserAddedOrRemovedCommand(DeviceAdminReceiver.ACTION_USER_ADDED, userHandle);
synchronized (DevicePolicyManagerService.this) {
@@ -1490,6 +1499,15 @@
mContext = context;
}
+ Context createContextAsUser(UserHandle user) throws PackageManager.NameNotFoundException {
+ final String packageName = mContext.getPackageName();
+ return mContext.createPackageContextAsUser(packageName, 0, user);
+ }
+
+ Resources getResources() {
+ return mContext.getResources();
+ }
+
Owners newOwners() {
return new Owners(getUserManager(), getUserManagerInternal(),
getPackageManagerInternal());
@@ -1725,6 +1743,10 @@
boolean securityLogIsLoggingEnabled() {
return SecurityLog.isLoggingEnabled();
}
+
+ KeyChainConnection keyChainBindAsUser(UserHandle user) throws InterruptedException {
+ return KeyChain.bindAsUser(mContext, user);
+ }
}
/**
@@ -1755,18 +1777,27 @@
.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN);
mIsWatch = mInjector.getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_WATCH);
+
+ // Broadcast filter for changes to the trusted certificate store. These changes get a
+ // separate intent filter so we can listen to them even when device_admin is off.
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_USER_STARTED);
+ filter.addAction(Intent.ACTION_USER_UNLOCKED);
+ filter.addAction(KeyChain.ACTION_TRUST_STORE_CHANGED);
+ filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ mContext.registerReceiverAsUser(mRootCaReceiver, UserHandle.ALL, filter, null, mHandler);
+
if (!mHasFeature) {
// Skip the rest of the initialization
return;
}
- IntentFilter filter = new IntentFilter();
+
+ filter = new IntentFilter();
filter.addAction(Intent.ACTION_BOOT_COMPLETED);
filter.addAction(ACTION_EXPIRED_PASSWORD_NOTIFICATION);
filter.addAction(Intent.ACTION_USER_ADDED);
filter.addAction(Intent.ACTION_USER_REMOVED);
filter.addAction(Intent.ACTION_USER_STARTED);
- filter.addAction(Intent.ACTION_USER_UNLOCKED);
- filter.addAction(KeyChain.ACTION_TRUST_STORE_CHANGED);
filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler);
filter = new IntentFilter();
@@ -2952,125 +2983,34 @@
}
}
- private class MonitoringCertNotificationTask extends AsyncTask<Integer, Void, Void> {
- @Override
- protected Void doInBackground(Integer... params) {
- int userHandle = params[0];
+ /**
+ * Remove deleted CA certificates from the "approved" list for a particular user, counting
+ * the number still remaining to approve.
+ *
+ * @param userHandle user to check for. This must be a real user and not, for example,
+ * {@link UserHandle#ALL}.
+ * @param installedCertificates the full set of certificate authorities currently installed for
+ * {@param userHandle}. After calling this function, {@code mAcceptedCaCertificates} will
+ * correspond to some subset of this.
+ *
+ * @return number of certificates yet to be approved by {@param userHandle}.
+ */
+ protected synchronized int retainAcceptedCertificates(final UserHandle userHandle,
+ final @NonNull Collection<String> installedCertificates) {
+ enforceManageUsers();
- if (userHandle == UserHandle.USER_ALL) {
- for (UserInfo userInfo : mUserManager.getUsers(true)) {
- manageNotification(userInfo.getUserHandle());
- }
- } else {
- manageNotification(UserHandle.of(userHandle));
- }
- return null;
- }
+ if (!mHasFeature) {
+ return installedCertificates.size();
+ } else {
+ final DevicePolicyData policy = getUserData(userHandle.getIdentifier());
- private void manageNotification(UserHandle userHandle) {
- if (!mUserManager.isUserUnlocked(userHandle)) {
- return;
+ // Remove deleted certificates. Flush xml if necessary.
+ if (policy.mAcceptedCaCertificates.retainAll(installedCertificates)) {
+ saveSettingsLocked(userHandle.getIdentifier());
}
- // Call out to KeyChain to check for CAs which are waiting for approval.
- final List<String> pendingCertificates;
- try {
- pendingCertificates = getInstalledCaCertificates(userHandle);
- } catch (RemoteException | RuntimeException e) {
- Log.e(LOG_TAG, "Could not retrieve certificates from KeyChain service", e);
- return;
- }
-
- synchronized (DevicePolicyManagerService.this) {
- final DevicePolicyData policy = getUserData(userHandle.getIdentifier());
-
- // Remove deleted certificates. Flush xml if necessary.
- if (policy.mAcceptedCaCertificates.retainAll(pendingCertificates)) {
- saveSettingsLocked(userHandle.getIdentifier());
- }
- // Trim to approved certificates.
- pendingCertificates.removeAll(policy.mAcceptedCaCertificates);
- }
-
- if (pendingCertificates.isEmpty()) {
- mInjector.getNotificationManager().cancelAsUser(
- null, MONITORING_CERT_NOTIFICATION_ID, userHandle);
- return;
- }
-
- // Build and show a warning notification
- int smallIconId;
- String contentText;
- int parentUserId = userHandle.getIdentifier();
- if (getProfileOwner(userHandle.getIdentifier()) != null) {
- contentText = mContext.getString(R.string.ssl_ca_cert_noti_managed,
- getProfileOwnerName(userHandle.getIdentifier()));
- smallIconId = R.drawable.stat_sys_certificate_info;
- parentUserId = getProfileParentId(userHandle.getIdentifier());
- } else if (getDeviceOwnerUserId() == userHandle.getIdentifier()) {
- contentText = mContext.getString(R.string.ssl_ca_cert_noti_managed,
- getDeviceOwnerName());
- smallIconId = R.drawable.stat_sys_certificate_info;
- } else {
- contentText = mContext.getString(R.string.ssl_ca_cert_noti_by_unknown);
- smallIconId = android.R.drawable.stat_sys_warning;
- }
-
- final int numberOfCertificates = pendingCertificates.size();
- Intent dialogIntent = new Intent(Settings.ACTION_MONITORING_CERT_INFO);
- dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
- dialogIntent.setPackage("com.android.settings");
- dialogIntent.putExtra(Settings.EXTRA_NUMBER_OF_CERTIFICATES, numberOfCertificates);
- dialogIntent.putExtra(Intent.EXTRA_USER_ID, userHandle.getIdentifier());
- PendingIntent notifyIntent = PendingIntent.getActivityAsUser(mContext, 0,
- dialogIntent, PendingIntent.FLAG_UPDATE_CURRENT, null,
- new UserHandle(parentUserId));
-
- final Context userContext;
- try {
- final String packageName = mContext.getPackageName();
- userContext = mContext.createPackageContextAsUser(packageName, 0, userHandle);
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(LOG_TAG, "Create context as " + userHandle + " failed", e);
- return;
- }
- final Notification noti = new Notification.Builder(userContext)
- .setSmallIcon(smallIconId)
- .setContentTitle(mContext.getResources().getQuantityText(
- R.plurals.ssl_ca_cert_warning, numberOfCertificates))
- .setContentText(contentText)
- .setContentIntent(notifyIntent)
- .setPriority(Notification.PRIORITY_HIGH)
- .setShowWhen(false)
- .setColor(mContext.getColor(
- com.android.internal.R.color.system_notification_accent_color))
- .build();
-
- mInjector.getNotificationManager().notifyAsUser(
- null, MONITORING_CERT_NOTIFICATION_ID, noti, userHandle);
- }
-
- private List<String> getInstalledCaCertificates(UserHandle userHandle)
- throws RemoteException, RuntimeException {
- KeyChainConnection conn = null;
- try {
- conn = KeyChain.bindAsUser(mContext, userHandle);
- List<ParcelableString> aliases = conn.getService().getUserCaAliases().getList();
- List<String> result = new ArrayList<>(aliases.size());
- for (int i = 0; i < aliases.size(); i++) {
- result.add(aliases.get(i).string);
- }
- return result;
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- return null;
- } catch (AssertionError e) {
- throw new RuntimeException(e);
- } finally {
- if (conn != null) {
- conn.close();
- }
- }
+ // Trim approved certificates from the count.
+ return installedCertificates.size() - policy.mAcceptedCaCertificates.size();
}
}
@@ -4635,7 +4575,7 @@
}
saveSettingsLocked(userId);
}
- new MonitoringCertNotificationTask().execute(userId);
+ new MonitoringCertNotificationTask(this, mInjector).execute(userId);
return true;
}
@@ -4659,7 +4599,7 @@
saveSettingsLocked(userInfo.id);
}
- new MonitoringCertNotificationTask().execute(userInfo.id);
+ new MonitoringCertNotificationTask(this, mInjector).execute(userInfo.id);
}
}
}
@@ -7154,7 +7094,7 @@
return UserHandle.isSameApp(mInjector.binderGetCallingUid(), Process.SYSTEM_UID);
}
- private int getProfileParentId(int userHandle) {
+ protected int getProfileParentId(int userHandle) {
final long ident = mInjector.binderClearCallingIdentity();
try {
UserInfo parentUser = mUserManager.getProfileParent(userHandle);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/MonitoringCertNotificationTask.java b/services/devicepolicy/java/com/android/server/devicepolicy/MonitoringCertNotificationTask.java
new file mode 100644
index 0000000..03c137a
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/MonitoringCertNotificationTask.java
@@ -0,0 +1,166 @@
+/*
+ * 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 com.android.server.devicepolicy;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.security.KeyChain.KeyChainConnection;
+import android.util.Log;
+
+import com.android.internal.R;
+import com.android.internal.util.ParcelableString;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class MonitoringCertNotificationTask extends AsyncTask<Integer, Void, Void> {
+ protected static final String LOG_TAG = DevicePolicyManagerService.LOG_TAG;
+ protected static final int MONITORING_CERT_NOTIFICATION_ID = R.plurals.ssl_ca_cert_warning;
+
+ private final DevicePolicyManagerService mService;
+ private final DevicePolicyManagerService.Injector mInjector;
+
+ public MonitoringCertNotificationTask(final DevicePolicyManagerService service,
+ final DevicePolicyManagerService.Injector injector) {
+ super();
+ mService = service;
+ mInjector = injector;
+ }
+
+ @Override
+ protected Void doInBackground(Integer... params) {
+ int userHandle = params[0];
+
+ if (userHandle == UserHandle.USER_ALL) {
+ for (UserInfo userInfo : mInjector.getUserManager().getUsers(true)) {
+ repostOrClearNotification(userInfo.getUserHandle());
+ }
+ } else {
+ repostOrClearNotification(UserHandle.of(userHandle));
+ }
+ return null;
+ }
+
+ private void repostOrClearNotification(UserHandle userHandle) {
+ if (!mInjector.getUserManager().isUserUnlocked(userHandle.getIdentifier())) {
+ return;
+ }
+
+ // Call out to KeyChain to check for CAs which are waiting for approval.
+ final int pendingCertificateCount;
+ try {
+ pendingCertificateCount = mService.retainAcceptedCertificates(
+ userHandle, getInstalledCaCertificates(userHandle));
+ } catch (RemoteException | RuntimeException e) {
+ Log.e(LOG_TAG, "Could not retrieve certificates from KeyChain service", e);
+ return;
+ }
+
+ if (pendingCertificateCount != 0) {
+ showNotification(userHandle, pendingCertificateCount);
+ } else {
+ mInjector.getNotificationManager().cancelAsUser(
+ LOG_TAG, MONITORING_CERT_NOTIFICATION_ID, userHandle);
+ }
+ }
+
+ private void showNotification(UserHandle userHandle, int pendingCertificateCount) {
+ // Create a context for the target user.
+ final Context userContext;
+ try {
+ userContext = mInjector.createContextAsUser(userHandle);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(LOG_TAG, "Create context as " + userHandle + " failed", e);
+ return;
+ }
+
+ // Build and show a warning notification
+ int smallIconId;
+ String contentText;
+ int parentUserId = userHandle.getIdentifier();
+ Resources resources = mInjector.getResources();
+ if (mService.getProfileOwner(userHandle.getIdentifier()) != null) {
+ contentText = resources.getString(R.string.ssl_ca_cert_noti_managed,
+ mService.getProfileOwnerName(userHandle.getIdentifier()));
+ smallIconId = R.drawable.stat_sys_certificate_info;
+ parentUserId = mService.getProfileParentId(userHandle.getIdentifier());
+ } else if (mService.getDeviceOwnerUserId() == userHandle.getIdentifier()) {
+ contentText = resources.getString(R.string.ssl_ca_cert_noti_managed,
+ mService.getDeviceOwnerName());
+ smallIconId = R.drawable.stat_sys_certificate_info;
+ } else {
+ contentText = resources.getString(R.string.ssl_ca_cert_noti_by_unknown);
+ smallIconId = android.R.drawable.stat_sys_warning;
+ }
+
+ Intent dialogIntent = new Intent(Settings.ACTION_MONITORING_CERT_INFO);
+ dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ // TODO this next line is taken from original notification code in
+ // {@link DevicePolicyManagerService} but not a very good way of doing it. Do it better.
+ dialogIntent.setPackage("com.android.settings");
+ dialogIntent.putExtra(Settings.EXTRA_NUMBER_OF_CERTIFICATES, pendingCertificateCount);
+ dialogIntent.putExtra(Intent.EXTRA_USER_ID, userHandle.getIdentifier());
+ PendingIntent notifyIntent = PendingIntent.getActivityAsUser(userContext, 0,
+ dialogIntent, PendingIntent.FLAG_UPDATE_CURRENT, null,
+ UserHandle.of(parentUserId));
+
+ final Notification noti = new Notification.Builder(userContext)
+ .setSmallIcon(smallIconId)
+ .setContentTitle(resources.getQuantityText(R.plurals.ssl_ca_cert_warning,
+ pendingCertificateCount))
+ .setContentText(contentText)
+ .setContentIntent(notifyIntent)
+ .setPriority(Notification.PRIORITY_HIGH)
+ .setShowWhen(false)
+ .setColor(R.color.system_notification_accent_color)
+ .build();
+
+ mInjector.getNotificationManager().notifyAsUser(
+ LOG_TAG, MONITORING_CERT_NOTIFICATION_ID, noti, userHandle);
+ }
+
+ private List<String> getInstalledCaCertificates(UserHandle userHandle)
+ throws RemoteException, RuntimeException {
+ try (KeyChainConnection conn = mInjector.keyChainBindAsUser(userHandle)) {
+ List<ParcelableString> aliases = conn.getService().getUserCaAliases().getList();
+ List<String> result = new ArrayList<>(aliases.size());
+ for (int i = 0; i < aliases.size(); i++) {
+ result.add(aliases.get(i).string);
+ }
+ return result;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return null;
+ } catch (AssertionError e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
index 3b92a34..e6dd13f 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
@@ -29,6 +29,7 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.os.UserManagerInternal;
+import android.security.KeyChain;
import android.telephony.TelephonyManager;
import android.util.ArrayMap;
import android.util.Pair;
@@ -375,5 +376,10 @@
boolean isBuildDebuggable() {
return context.buildMock.isDebuggable;
}
+
+ @Override
+ KeyChain.KeyChainConnection keyChainBindAsUser(UserHandle user) {
+ return context.keyChainConnection;
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 6fb65d5..a186b59 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -21,6 +21,8 @@
import android.Manifest.permission;
import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationManager;
import android.app.admin.DeviceAdminReceiver;
import android.app.admin.DevicePolicyManager;
import android.app.admin.DevicePolicyManagerInternal;
@@ -32,6 +34,7 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
import android.content.res.Resources;
import android.graphics.Color;
import android.net.IIpConnectivityMetrics;
@@ -52,10 +55,13 @@
import android.util.Pair;
import com.android.internal.R;
+import com.android.internal.util.ParcelableString;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.pm.UserRestrictionsUtils;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
@@ -76,13 +82,16 @@
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.argThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@@ -1193,6 +1202,53 @@
return uid;
}
+ public void testCertificateDisclosure() throws Exception {
+ final int userId = DpmMockContext.CALLER_USER_HANDLE;
+ final UserHandle user = UserHandle.of(userId);
+
+ mContext.applicationInfo = new ApplicationInfo();
+ mContext.callerPermissions.add(permission.MANAGE_USERS);
+ mContext.packageName = "com.android.frameworks.servicestests";
+ mContext.userContexts.put(user, mContext);
+ when(mContext.resources.getColor(anyInt(), anyObject())).thenReturn(Color.WHITE);
+
+ ParceledListSlice<ParcelableString> oneCert = asSlice(new String[] {"1"});
+ ParceledListSlice<ParcelableString> fourCerts = asSlice(new String[] {"1", "2", "3", "4"});
+
+ final String TEST_STRING = "Test for exactly 2 certs out of 4";
+ doReturn(TEST_STRING).when(mContext.resources).getQuantityText(anyInt(), eq(2));
+
+ // Given that we have exactly one certificate installed,
+ when(mContext.keyChainConnection.getService().getUserCaAliases()).thenReturn(oneCert);
+ // when that certificate is approved,
+ dpms.approveCaCert(oneCert.getList().get(0).string, userId, true);
+ // a notification should not be shown.
+ verify(mContext.notificationManager, timeout(1000))
+ .cancelAsUser(anyString(), anyInt(), eq(user));
+
+ // Given that we have four certificates installed,
+ when(mContext.keyChainConnection.getService().getUserCaAliases()).thenReturn(fourCerts);
+ // when two of them are approved (one of them approved twice hence no action),
+ dpms.approveCaCert(fourCerts.getList().get(0).string, userId, true);
+ dpms.approveCaCert(fourCerts.getList().get(1).string, userId, true);
+ // a notification should be shown saying that there are two certificates left to approve.
+ verify(mContext.notificationManager, timeout(1000))
+ .notifyAsUser(anyString(), anyInt(), argThat(
+ new BaseMatcher<Notification>() {
+ @Override
+ public boolean matches(Object item) {
+ final Notification noti = (Notification) item;
+ return TEST_STRING.equals(
+ noti.extras.getString(Notification.EXTRA_TITLE));
+ }
+ @Override
+ public void describeTo(Description description) {
+ description.appendText(
+ "Notification{title=\"" + TEST_STRING + "\"}");
+ }
+ }), eq(user));
+ }
+
/**
* Simple test for delegate set/get and general delegation. Tests verifying that delegated
* privileges can acually be exercised by a delegate are not covered here.
@@ -3734,4 +3790,20 @@
assertTrue(dpm.setProfileOwner(admin, null, userId));
mContext.callerPermissions.removeAll(OWNER_SETUP_PERMISSIONS);
}
+
+ /**
+ * Convert String[] to ParceledListSlice<ParcelableString>.
+ * <p>
+ * TODO: This shouldn't be necessary. If ParcelableString does need to exist, it also needs
+ * a real constructor.
+ */
+ private static ParceledListSlice<ParcelableString> asSlice(String[] s) {
+ List<ParcelableString> list = new ArrayList<>(s.length);
+ for (int i = 0; i < s.length; i++) {
+ ParcelableString item = new ParcelableString();
+ item.string = s[i];
+ list.add(i, item);
+ }
+ return new ParceledListSlice<ParcelableString>(list);
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
index 22cd135..46aaf83 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
@@ -43,9 +43,11 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.os.UserManagerInternal;
+import android.security.KeyChain;
import android.telephony.TelephonyManager;
import android.test.mock.MockContentResolver;
import android.test.mock.MockContext;
+import android.util.ArrayMap;
import android.view.IWindowManager;
import com.android.internal.widget.LockPatternUtils;
@@ -58,10 +60,12 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
@@ -290,6 +294,7 @@
public final TelephonyManager telephonyManager;
public final AccountManager accountManager;
public final AlarmManager alarmManager;
+ public final KeyChain.KeyChainConnection keyChainConnection;
/** Note this is a partial mock, not a real mock. */
public final PackageManager packageManager;
@@ -300,6 +305,9 @@
public final BuildMock buildMock = new BuildMock();
+ /** Optional mapping of other user contexts for {@link #createPackageContextAsUser} to return */
+ public final Map<UserHandle, Context> userContexts = new ArrayMap<>();
+
public String packageName = null;
public ApplicationInfo applicationInfo = null;
@@ -335,6 +343,7 @@
telephonyManager = mock(TelephonyManager.class);
accountManager = mock(AccountManager.class);
alarmManager = mock(AlarmManager.class);
+ keyChainConnection = mock(KeyChain.KeyChainConnection.class, RETURNS_DEEP_STUBS);
// Package manager is huge, so we use a partial mock instead.
packageManager = spy(context.getPackageManager());
@@ -690,6 +699,19 @@
}
@Override
+ public Context createPackageContextAsUser(String packageName, int flags, UserHandle user)
+ throws PackageManager.NameNotFoundException {
+ if (!userContexts.containsKey(user)) {
+ return super.createPackageContextAsUser(packageName, flags, user);
+ }
+ if (!getPackageName().equals(packageName)) {
+ throw new UnsupportedOperationException(
+ "Creating a context as another package is not implemented");
+ }
+ return userContexts.get(user);
+ }
+
+ @Override
public ContentResolver getContentResolver() {
return contentResolver;
}
diff --git a/tests/net/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java b/tests/net/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java
index 0fa4545..32e1b96 100644
--- a/tests/net/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java
+++ b/tests/net/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java
@@ -210,10 +210,9 @@
inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNMService).enableNat(IFACE_NAME, UPSTREAM_IFACE2);
- // TODO: Verify proper cleanup is performed:
- // inOrder.verify(mStatsService).forceUpdate();
- // inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE2);
- // inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mStatsService).forceUpdate();
+ inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE2);
}
@Test
@@ -230,10 +229,9 @@
inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNMService).enableNat(IFACE_NAME, UPSTREAM_IFACE2);
inOrder.verify(mNMService).startInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE2);
- // TODO: Verify proper cleanup is performed:
- // inOrder.verify(mStatsService).forceUpdate();
- // inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE2);
- // inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mStatsService).forceUpdate();
+ inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE2);
}
@Test
@@ -296,6 +294,18 @@
IFACE_NAME, mTestedSm, STATE_AVAILABLE, TETHER_ERROR_ENABLE_NAT_ERROR);
}
+ @Test
+ public void ignoresDuplicateUpstreamNotifications() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
+
+ verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
+
+ for (int i = 0; i < 5; i++) {
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+ verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
+ }
+ }
+
/**
* Send a command to the state machine under test, and run the event loop to idle.
*
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java
index 77b131f..4ffb2e2 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java
@@ -132,7 +132,7 @@
@Override
public Result insertChild(Object parentView, ILayoutPullParser childXml, int index,
IAnimationListener listener) {
- if (parentView instanceof ViewGroup == false) {
+ if (!(parentView instanceof ViewGroup)) {
throw new IllegalArgumentException("parentView is not a ViewGroup");
}
@@ -155,10 +155,10 @@
@Override
public Result moveChild(Object parentView, Object childView, int index,
Map<String, String> layoutParams, IAnimationListener listener) {
- if (parentView instanceof ViewGroup == false) {
+ if (!(parentView instanceof ViewGroup)) {
throw new IllegalArgumentException("parentView is not a ViewGroup");
}
- if (childView instanceof View == false) {
+ if (!(childView instanceof View)) {
throw new IllegalArgumentException("childView is not a View");
}
@@ -179,7 +179,7 @@
@Override
public Result removeChild(Object childView, IAnimationListener listener) {
- if (childView instanceof View == false) {
+ if (!(childView instanceof View)) {
throw new IllegalArgumentException("childView is not a View");
}
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java
index d392f21..91668af 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java
@@ -100,7 +100,7 @@
mView.setText(text);
}
- public void setGravity(int gravity) {
+ private void setGravity(int gravity) {
mView.setGravity(gravity);
}
}
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/Layout.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/Layout.java
index 2fe3ed5..287334c 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/Layout.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/Layout.java
@@ -305,6 +305,7 @@
return Bridge.getResourceId(ResourceType.ID, ID_PREFIX + name);
}
+ @SuppressWarnings("deprecation")
@Override
public void requestFitSystemWindows() {
// The framework call would usually bubble up to ViewRootImpl but, in layoutlib, Layout will
@@ -416,6 +417,7 @@
}
}
+ @SuppressWarnings("SameParameterValue")
private int getDimension(String attr, boolean isFramework, int defaultValue) {
ResourceValue value = mResources.findItemInTheme(attr, isFramework);
value = mResources.resolveResValue(value);
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java
index 85fe2a4..d21955e 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java
@@ -144,7 +144,7 @@
private static final class PostInflateException extends Exception {
private static final long serialVersionUID = 1L;
- public PostInflateException(String message) {
+ private PostInflateException(String message) {
super(message);
}
}
@@ -242,11 +242,13 @@
// Then measure only the content with UNSPECIFIED to see the size difference
// and apply this to the screen size.
+ View measuredView = mContentRoot.getChildAt(0);
+
// first measure the full layout, with EXACTLY to get the size of the
// content as it is inside the decor/dialog
@SuppressWarnings("deprecation")
Pair<Integer, Integer> exactMeasure = measureView(
- mViewRoot, mContentRoot.getChildAt(0),
+ mViewRoot, measuredView,
mMeasuredScreenWidth, MeasureSpec.EXACTLY,
mMeasuredScreenHeight, MeasureSpec.EXACTLY);
@@ -258,6 +260,10 @@
mMeasuredScreenWidth, widthMeasureSpecMode,
mMeasuredScreenHeight, heightMeasureSpecMode);
+ // If measuredView is not null, exactMeasure nor result will be null.
+ assert exactMeasure != null;
+ assert result != null;
+
// now look at the difference and add what is needed.
if (renderingMode.isHorizExpand()) {
int measuredWidth = exactMeasure.getFirst();
@@ -406,8 +412,7 @@
* @param canvas an optional canvas to render the views to. If null, only the measure and
* layout steps will be executed.
*/
- private static Result renderAndBuildResult(@NonNull BridgeContext context, @NonNull ViewGroup viewRoot,
- @Nullable Canvas canvas, int width, int height) {
+ private static Result renderAndBuildResult(@NonNull ViewGroup viewRoot, @Nullable Canvas canvas) {
if (canvas == null) {
return SUCCESS.createResult();
}
@@ -551,7 +556,7 @@
long initialTime = System_Delegate.nanoTime();
if (!mFirstFrameExecuted) {
// We need to run an initial draw call to initialize the animations
- renderAndBuildResult(getContext(), mViewRoot, NOP_CANVAS, mMeasuredScreenWidth, mMeasuredScreenHeight);
+ renderAndBuildResult(mViewRoot, NOP_CANVAS);
// The first frame will initialize the animations
Choreographer_Delegate.doFrame(initialTime);
@@ -560,8 +565,7 @@
// Second frame will move the animations
Choreographer_Delegate.doFrame(initialTime + mElapsedFrameTimeNanos);
}
- renderResult = renderAndBuildResult(getContext(), mViewRoot, mCanvas, mMeasuredScreenWidth,
- mMeasuredScreenHeight);
+ renderResult = renderAndBuildResult(mViewRoot, mCanvas);
}
mSystemViewInfoList =
@@ -1206,7 +1210,7 @@
* Sets up a {@link TabHost} object.
* @param tabHost the TabHost to setup.
* @param layoutlibCallback The project callback object to access the project R class.
- * @throws PostInflateException
+ * @throws PostInflateException if TabHost is missing the required ids for TabHost
*/
private void setupTabHost(TabHost tabHost, LayoutlibCallback layoutlibCallback)
throws PostInflateException {
@@ -1254,12 +1258,7 @@
TabSpec spec = tabHost.newTabSpec("tag")
.setIndicator("Tab Label", tabHost.getResources()
.getDrawable(android.R.drawable.ic_menu_info_details, null))
- .setContent(new TabHost.TabContentFactory() {
- @Override
- public View createTabContent(String tag) {
- return new LinearLayout(getContext());
- }
- });
+ .setContent(tag -> new LinearLayout(getContext()));
tabHost.addTab(spec);
} else {
// for each child of the frameLayout, add a new TabSpec
@@ -1333,8 +1332,8 @@
int childCount = viewGroup.getChildCount();
if (viewGroup == mContentRoot) {
- List<ViewInfo> childrenWithoutOffset = new ArrayList<ViewInfo>(childCount);
- List<ViewInfo> childrenWithOffset = new ArrayList<ViewInfo>(childCount);
+ List<ViewInfo> childrenWithoutOffset = new ArrayList<>(childCount);
+ List<ViewInfo> childrenWithOffset = new ArrayList<>(childCount);
for (int i = 0; i < childCount; i++) {
ViewInfo[] childViewInfo =
visitContentRoot(viewGroup.getChildAt(i), hOffset, vOffset,
@@ -1345,7 +1344,7 @@
mViewInfoList = childrenWithOffset;
return childrenWithoutOffset;
} else {
- List<ViewInfo> children = new ArrayList<ViewInfo>(childCount);
+ List<ViewInfo> children = new ArrayList<>(childCount);
for (int i = 0; i < childCount; i++) {
children.add(visit(viewGroup.getChildAt(i), hOffset, vOffset, setExtendedInfo,
isContentFrame));
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/NinePatchInputStream.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/NinePatchInputStream.java
index 96b795a..f149b6c 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/NinePatchInputStream.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/NinePatchInputStream.java
@@ -34,12 +34,9 @@
@Override
public boolean markSupported() {
- if (mFakeMarkSupport) {
- // this is needed so that BitmapFactory doesn't wrap this in a BufferedInputStream.
- return true;
- }
+ // this is needed so that BitmapFactory doesn't wrap this in a BufferedInputStream.
+ return mFakeMarkSupport || super.markSupported();
- return super.markSupported();
}
public void disableFakeMarkSupport() {
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/ReflectionUtils.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/ReflectionUtils.java
index 040191e..b89718f 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/ReflectionUtils.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/ReflectionUtils.java
@@ -52,9 +52,7 @@
Exception ex;
try {
return method.invoke(object, args);
- } catch (IllegalAccessException e) {
- ex = e;
- } catch (InvocationTargetException e) {
+ } catch (IllegalAccessException | InvocationTargetException e) {
ex = e;
}
throw new ReflectionException(ex);