diff options
407 files changed, 12944 insertions, 5794 deletions
diff --git a/Android.mk b/Android.mk index 121a38efd587..e352bc70d085 100644 --- a/Android.mk +++ b/Android.mk @@ -332,6 +332,7 @@ LOCAL_SRC_FILES += \ core/java/com/android/internal/os/IDropBoxManagerService.aidl \ core/java/com/android/internal/os/IParcelFileDescriptorFactory.aidl \ core/java/com/android/internal/os/IResultReceiver.aidl \ + core/java/com/android/internal/os/IShellCallback.aidl \ core/java/com/android/internal/statusbar/IStatusBar.aidl \ core/java/com/android/internal/statusbar/IStatusBarService.aidl \ core/java/com/android/internal/textservice/ISpellCheckerService.aidl \ diff --git a/api/current.txt b/api/current.txt index 2d3fde86de38..3d116029ad17 100644 --- a/api/current.txt +++ b/api/current.txt @@ -4042,6 +4042,7 @@ package android.app { field public static final java.lang.String OPSTR_MOCK_LOCATION = "android:mock_location"; field public static final java.lang.String OPSTR_MONITOR_HIGH_POWER_LOCATION = "android:monitor_location_high_power"; field public static final java.lang.String OPSTR_MONITOR_LOCATION = "android:monitor_location"; + field public static final java.lang.String OPSTR_PROCESS_OUTGOING_CALLS = "android:process_outgoing_calls"; field public static final java.lang.String OPSTR_READ_CALENDAR = "android:read_calendar"; field public static final java.lang.String OPSTR_READ_CALL_LOG = "android:read_call_log"; field public static final java.lang.String OPSTR_READ_CELL_BROADCASTS = "android:read_cell_broadcasts"; @@ -36915,6 +36916,7 @@ package android.telephony { field public static final java.lang.String KEY_DTMF_TYPE_ENABLED_BOOL = "dtmf_type_enabled_bool"; field public static final java.lang.String KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT = "duration_blocking_disabled_after_emergency_int"; field public static final java.lang.String KEY_EDITABLE_ENHANCED_4G_LTE_BOOL = "editable_enhanced_4g_lte_bool"; + field public static final java.lang.String KEY_EDITABLE_VOICEMAIL_NUMBER_BOOL = "editable_voicemail_number_bool"; field public static final java.lang.String KEY_ENABLE_DIALER_KEY_VIBRATION_BOOL = "enable_dialer_key_vibration_bool"; field public static final java.lang.String KEY_FORCE_HOME_NETWORK_BOOL = "force_home_network_bool"; field public static final java.lang.String KEY_GSM_DTMF_TONE_DELAY_INT = "gsm_dtmf_tone_delay_int"; @@ -52070,6 +52072,8 @@ package java.lang.annotation { enum_constant public static final java.lang.annotation.ElementType PACKAGE; enum_constant public static final java.lang.annotation.ElementType PARAMETER; enum_constant public static final java.lang.annotation.ElementType TYPE; + enum_constant public static final java.lang.annotation.ElementType TYPE_PARAMETER; + enum_constant public static final java.lang.annotation.ElementType TYPE_USE; } public class IncompleteAnnotationException extends java.lang.RuntimeException { @@ -52151,7 +52155,7 @@ package java.lang.reflect { method public abstract <T extends java.lang.annotation.Annotation> T getAnnotation(java.lang.Class<T>); method public abstract java.lang.annotation.Annotation[] getAnnotations(); method public default <T extends java.lang.annotation.Annotation> T[] getAnnotationsByType(java.lang.Class<T>); - method public default <T extends java.lang.annotation.Annotation> java.lang.annotation.Annotation getDeclaredAnnotation(java.lang.Class<T>); + method public default <T extends java.lang.annotation.Annotation> T getDeclaredAnnotation(java.lang.Class<T>); method public abstract java.lang.annotation.Annotation[] getDeclaredAnnotations(); method public default <T extends java.lang.annotation.Annotation> T[] getDeclaredAnnotationsByType(java.lang.Class<T>); method public default boolean isAnnotationPresent(java.lang.Class<? extends java.lang.annotation.Annotation>); diff --git a/api/system-current.txt b/api/system-current.txt index d0116eedaf6b..1ab4884ed736 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -4172,6 +4172,7 @@ package android.app { field public static final java.lang.String OPSTR_MOCK_LOCATION = "android:mock_location"; field public static final java.lang.String OPSTR_MONITOR_HIGH_POWER_LOCATION = "android:monitor_location_high_power"; field public static final java.lang.String OPSTR_MONITOR_LOCATION = "android:monitor_location"; + field public static final java.lang.String OPSTR_PROCESS_OUTGOING_CALLS = "android:process_outgoing_calls"; field public static final java.lang.String OPSTR_READ_CALENDAR = "android:read_calendar"; field public static final java.lang.String OPSTR_READ_CALL_LOG = "android:read_call_log"; field public static final java.lang.String OPSTR_READ_CELL_BROADCASTS = "android:read_cell_broadcasts"; @@ -40006,6 +40007,7 @@ package android.telephony { field public static final java.lang.String KEY_DTMF_TYPE_ENABLED_BOOL = "dtmf_type_enabled_bool"; field public static final java.lang.String KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT = "duration_blocking_disabled_after_emergency_int"; field public static final java.lang.String KEY_EDITABLE_ENHANCED_4G_LTE_BOOL = "editable_enhanced_4g_lte_bool"; + field public static final java.lang.String KEY_EDITABLE_VOICEMAIL_NUMBER_BOOL = "editable_voicemail_number_bool"; field public static final java.lang.String KEY_ENABLE_DIALER_KEY_VIBRATION_BOOL = "enable_dialer_key_vibration_bool"; field public static final java.lang.String KEY_FORCE_HOME_NETWORK_BOOL = "force_home_network_bool"; field public static final java.lang.String KEY_GSM_DTMF_TONE_DELAY_INT = "gsm_dtmf_tone_delay_int"; @@ -55607,6 +55609,8 @@ package java.lang.annotation { enum_constant public static final java.lang.annotation.ElementType PACKAGE; enum_constant public static final java.lang.annotation.ElementType PARAMETER; enum_constant public static final java.lang.annotation.ElementType TYPE; + enum_constant public static final java.lang.annotation.ElementType TYPE_PARAMETER; + enum_constant public static final java.lang.annotation.ElementType TYPE_USE; } public class IncompleteAnnotationException extends java.lang.RuntimeException { @@ -55688,7 +55692,7 @@ package java.lang.reflect { method public abstract <T extends java.lang.annotation.Annotation> T getAnnotation(java.lang.Class<T>); method public abstract java.lang.annotation.Annotation[] getAnnotations(); method public default <T extends java.lang.annotation.Annotation> T[] getAnnotationsByType(java.lang.Class<T>); - method public default <T extends java.lang.annotation.Annotation> java.lang.annotation.Annotation getDeclaredAnnotation(java.lang.Class<T>); + method public default <T extends java.lang.annotation.Annotation> T getDeclaredAnnotation(java.lang.Class<T>); method public abstract java.lang.annotation.Annotation[] getDeclaredAnnotations(); method public default <T extends java.lang.annotation.Annotation> T[] getDeclaredAnnotationsByType(java.lang.Class<T>); method public default boolean isAnnotationPresent(java.lang.Class<? extends java.lang.annotation.Annotation>); diff --git a/api/test-current.txt b/api/test-current.txt index 9c744c50a6f4..6d4590def9c3 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -4045,6 +4045,7 @@ package android.app { field public static final java.lang.String OPSTR_MOCK_LOCATION = "android:mock_location"; field public static final java.lang.String OPSTR_MONITOR_HIGH_POWER_LOCATION = "android:monitor_location_high_power"; field public static final java.lang.String OPSTR_MONITOR_LOCATION = "android:monitor_location"; + field public static final java.lang.String OPSTR_PROCESS_OUTGOING_CALLS = "android:process_outgoing_calls"; field public static final java.lang.String OPSTR_READ_CALENDAR = "android:read_calendar"; field public static final java.lang.String OPSTR_READ_CALL_LOG = "android:read_call_log"; field public static final java.lang.String OPSTR_READ_CELL_BROADCASTS = "android:read_cell_broadcasts"; @@ -36996,6 +36997,7 @@ package android.telephony { field public static final java.lang.String KEY_DTMF_TYPE_ENABLED_BOOL = "dtmf_type_enabled_bool"; field public static final java.lang.String KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT = "duration_blocking_disabled_after_emergency_int"; field public static final java.lang.String KEY_EDITABLE_ENHANCED_4G_LTE_BOOL = "editable_enhanced_4g_lte_bool"; + field public static final java.lang.String KEY_EDITABLE_VOICEMAIL_NUMBER_BOOL = "editable_voicemail_number_bool"; field public static final java.lang.String KEY_ENABLE_DIALER_KEY_VIBRATION_BOOL = "enable_dialer_key_vibration_bool"; field public static final java.lang.String KEY_FORCE_HOME_NETWORK_BOOL = "force_home_network_bool"; field public static final java.lang.String KEY_GSM_DTMF_TONE_DELAY_INT = "gsm_dtmf_tone_delay_int"; @@ -52172,6 +52174,8 @@ package java.lang.annotation { enum_constant public static final java.lang.annotation.ElementType PACKAGE; enum_constant public static final java.lang.annotation.ElementType PARAMETER; enum_constant public static final java.lang.annotation.ElementType TYPE; + enum_constant public static final java.lang.annotation.ElementType TYPE_PARAMETER; + enum_constant public static final java.lang.annotation.ElementType TYPE_USE; } public class IncompleteAnnotationException extends java.lang.RuntimeException { @@ -52253,7 +52257,7 @@ package java.lang.reflect { method public abstract <T extends java.lang.annotation.Annotation> T getAnnotation(java.lang.Class<T>); method public abstract java.lang.annotation.Annotation[] getAnnotations(); method public default <T extends java.lang.annotation.Annotation> T[] getAnnotationsByType(java.lang.Class<T>); - method public default <T extends java.lang.annotation.Annotation> java.lang.annotation.Annotation getDeclaredAnnotation(java.lang.Class<T>); + method public default <T extends java.lang.annotation.Annotation> T getDeclaredAnnotation(java.lang.Class<T>); method public abstract java.lang.annotation.Annotation[] getDeclaredAnnotations(); method public default <T extends java.lang.annotation.Annotation> T[] getDeclaredAnnotationsByType(java.lang.Class<T>); method public default boolean isAnnotationPresent(java.lang.Class<? extends java.lang.annotation.Annotation>); diff --git a/cmds/am/src/com/android/commands/am/Am.java b/cmds/am/src/com/android/commands/am/Am.java index 91334bd87296..3759de2e7197 100644 --- a/cmds/am/src/com/android/commands/am/Am.java +++ b/cmds/am/src/com/android/commands/am/Am.java @@ -26,7 +26,6 @@ import static android.app.ActivityManager.StackId.INVALID_STACK_ID; import android.app.ActivityManager; import android.app.ActivityManager.StackInfo; import android.app.ActivityManagerNative; -import android.app.ActivityOptions; import android.app.IActivityContainer; import android.app.IActivityController; import android.app.IActivityManager; @@ -46,7 +45,6 @@ import android.content.Intent; import android.content.pm.IPackageManager; import android.content.pm.InstrumentationInfo; import android.content.pm.ParceledListSlice; -import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.content.res.Configuration; import android.graphics.Rect; @@ -55,10 +53,11 @@ import android.os.Build; import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.os.RemoteException; +import android.os.ResultReceiver; import android.os.SELinux; import android.os.ServiceManager; +import android.os.ShellCallback; import android.os.ShellCommand; -import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; import android.text.TextUtils; @@ -72,6 +71,7 @@ import com.android.internal.util.Preconditions; import java.io.BufferedReader; import java.io.File; +import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; @@ -96,6 +96,7 @@ public class Am extends BaseCommand { // Amount we reduce the stack size by when testing a task re-size. private static final int STACK_BOUNDS_INSET = 10; + public static final String NO_CLASS_ERROR_CODE = "Error type 3"; private IActivityManager mAm; private IPackageManager mPm; @@ -198,7 +199,7 @@ public class Am extends BaseCommand { " --track-allocation: enable tracking of object allocations\n" + " --user <USER_ID> | current: Specify which user to run as; if not\n" + " specified then run as the current user.\n" + - " --stack <STACK_ID>: Specify into which stack should the activity be put." + + " --stack <STACK_ID>: Specify into which stack should the activity be put.\n" + "\n" + "am startservice: start a Service. Options are:\n" + " --user <USER_ID> | current: Specify which user to run as; if not\n" + @@ -385,17 +386,13 @@ public class Am extends BaseCommand { String op = nextArgRequired(); if (op.equals("start")) { - runStart(); + runAmCmd(getRawArgs()); } else if (op.equals("startservice")) { runStartService(); } else if (op.equals("stopservice")) { runStopService(); - } else if (op.equals("force-stop")) { - runForceStop(); - } else if (op.equals("kill")) { - runKill(); - } else if (op.equals("kill-all")) { - runKillAll(); + } else if (op.equals("force-stop") || op.equals("kill") || op.equals("kill-all")) { + runAmCmd(getRawArgs()); } else if (op.equals("instrument")) { runInstrument(); } else if (op.equals("trace-ipc")) { @@ -475,6 +472,49 @@ public class Am extends BaseCommand { return userId; } + static final class MyShellCallback extends ShellCallback { + @Override public ParcelFileDescriptor onOpenOutputFile(String path, String seLinuxContext) { + File file = new File(path); + //System.err.println("Opening file: " + file.getAbsolutePath()); + //Log.i("Am", "Opening file: " + file.getAbsolutePath()); + final ParcelFileDescriptor fd; + try { + fd = ParcelFileDescriptor.open(file, + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE | + ParcelFileDescriptor.MODE_WRITE_ONLY); + } catch (FileNotFoundException e) { + String msg = "Unable to open file " + path + ": " + e; + System.err.println(msg); + throw new IllegalArgumentException(msg); + } + if (seLinuxContext != null) { + final String tcon = SELinux.getFileContext(file.getAbsolutePath()); + if (!SELinux.checkSELinuxAccess(seLinuxContext, tcon, "file", "write")) { + try { + fd.close(); + } catch (IOException e) { + } + String msg = "System server has no access to file context " + tcon; + System.err.println(msg + " (from path " + file.getAbsolutePath() + + ", context " + seLinuxContext + ")"); + throw new IllegalArgumentException(msg); + } + } + return fd; + } + } + + void runAmCmd(String[] args) throws AndroidException { + try { + mAm.asBinder().shellCommand(FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, + args, new MyShellCallback(), new ResultReceiver(null) { }); + } catch (RemoteException e) { + System.err.println(NO_SYSTEM_ERROR_CODE); + throw new AndroidException("Can't call activity manager; is the system running?"); + } + } + private Intent makeIntent(int defUser) throws URISyntaxException { mStartFlags = 0; mWaitOption = false; @@ -558,211 +598,6 @@ public class Am extends BaseCommand { } } - private void runStart() throws Exception { - Intent intent = makeIntent(UserHandle.USER_CURRENT); - - if (mUserId == UserHandle.USER_ALL) { - System.err.println("Error: Can't start service with user 'all'"); - return; - } - - String mimeType = intent.getType(); - if (mimeType == null && intent.getData() != null - && "content".equals(intent.getData().getScheme())) { - mimeType = mAm.getProviderMimeType(intent.getData(), mUserId); - } - - - do { - if (mStopOption) { - String packageName; - if (intent.getComponent() != null) { - packageName = intent.getComponent().getPackageName(); - } else { - List<ResolveInfo> activities = mPm.queryIntentActivities(intent, mimeType, 0, - mUserId).getList(); - if (activities == null || activities.size() <= 0) { - System.err.println("Error: Intent does not match any activities: " - + intent); - return; - } else if (activities.size() > 1) { - System.err.println("Error: Intent matches multiple activities; can't stop: " - + intent); - return; - } - packageName = activities.get(0).activityInfo.packageName; - } - System.out.println("Stopping: " + packageName); - mAm.forceStopPackage(packageName, mUserId); - Thread.sleep(250); - } - - System.out.println("Starting: " + intent); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - ParcelFileDescriptor fd = null; - ProfilerInfo profilerInfo = null; - - if (mProfileFile != null) { - try { - fd = openForSystemServer( - new File(mProfileFile), - ParcelFileDescriptor.MODE_CREATE | - ParcelFileDescriptor.MODE_TRUNCATE | - ParcelFileDescriptor.MODE_WRITE_ONLY); - } catch (FileNotFoundException e) { - System.err.println("Error: Unable to open file: " + mProfileFile); - System.err.println("Consider using a file under /data/local/tmp/"); - return; - } - profilerInfo = new ProfilerInfo(mProfileFile, fd, mSamplingInterval, mAutoStop); - } - - IActivityManager.WaitResult result = null; - int res; - final long startTime = SystemClock.uptimeMillis(); - ActivityOptions options = null; - if (mStackId != INVALID_STACK_ID) { - options = ActivityOptions.makeBasic(); - options.setLaunchStackId(mStackId); - } - if (mWaitOption) { - result = mAm.startActivityAndWait(null, null, intent, mimeType, - null, null, 0, mStartFlags, profilerInfo, - options != null ? options.toBundle() : null, mUserId); - res = result.result; - } else { - res = mAm.startActivityAsUser(null, null, intent, mimeType, - null, null, 0, mStartFlags, profilerInfo, - options != null ? options.toBundle() : null, mUserId); - } - final long endTime = SystemClock.uptimeMillis(); - PrintStream out = mWaitOption ? System.out : System.err; - boolean launched = false; - switch (res) { - case ActivityManager.START_SUCCESS: - launched = true; - break; - case ActivityManager.START_SWITCHES_CANCELED: - launched = true; - out.println( - "Warning: Activity not started because the " - + " current activity is being kept for the user."); - break; - case ActivityManager.START_DELIVERED_TO_TOP: - launched = true; - out.println( - "Warning: Activity not started, intent has " - + "been delivered to currently running " - + "top-most instance."); - break; - case ActivityManager.START_RETURN_INTENT_TO_CALLER: - launched = true; - out.println( - "Warning: Activity not started because intent " - + "should be handled by the caller"); - break; - case ActivityManager.START_TASK_TO_FRONT: - launched = true; - out.println( - "Warning: Activity not started, its current " - + "task has been brought to the front"); - break; - case ActivityManager.START_INTENT_NOT_RESOLVED: - out.println( - "Error: Activity not started, unable to " - + "resolve " + intent.toString()); - break; - case ActivityManager.START_CLASS_NOT_FOUND: - out.println(NO_CLASS_ERROR_CODE); - out.println("Error: Activity class " + - intent.getComponent().toShortString() - + " does not exist."); - break; - case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT: - out.println( - "Error: Activity not started, you requested to " - + "both forward and receive its result"); - break; - case ActivityManager.START_PERMISSION_DENIED: - out.println( - "Error: Activity not started, you do not " - + "have permission to access it."); - break; - case ActivityManager.START_NOT_VOICE_COMPATIBLE: - out.println( - "Error: Activity not started, voice control not allowed for: " - + intent); - break; - case ActivityManager.START_NOT_CURRENT_USER_ACTIVITY: - out.println( - "Error: Not allowed to start background user activity" - + " that shouldn't be displayed for all users."); - break; - default: - out.println( - "Error: Activity not started, unknown error code " + res); - break; - } - if (mWaitOption && launched) { - if (result == null) { - result = new IActivityManager.WaitResult(); - result.who = intent.getComponent(); - } - System.out.println("Status: " + (result.timeout ? "timeout" : "ok")); - if (result.who != null) { - System.out.println("Activity: " + result.who.flattenToShortString()); - } - if (result.thisTime >= 0) { - System.out.println("ThisTime: " + result.thisTime); - } - if (result.totalTime >= 0) { - System.out.println("TotalTime: " + result.totalTime); - } - System.out.println("WaitTime: " + (endTime-startTime)); - System.out.println("Complete"); - } - mRepeat--; - if (mRepeat > 0) { - mAm.unhandledBack(); - } - } while (mRepeat > 0); - } - - private void runForceStop() throws Exception { - int userId = UserHandle.USER_ALL; - - String opt; - while ((opt=nextOption()) != null) { - if (opt.equals("--user")) { - userId = parseUserArg(nextArgRequired()); - } else { - System.err.println("Error: Unknown option: " + opt); - return; - } - } - mAm.forceStopPackage(nextArgRequired(), userId); - } - - private void runKill() throws Exception { - int userId = UserHandle.USER_ALL; - - String opt; - while ((opt=nextOption()) != null) { - if (opt.equals("--user")) { - userId = parseUserArg(nextArgRequired()); - } else { - System.err.println("Error: Unknown option: " + opt); - return; - } - } - mAm.killBackgroundProcesses(nextArgRequired(), userId); - } - - private void runKillAll() throws Exception { - mAm.killAllBackgroundProcesses(); - } - private void sendBroadcast() throws Exception { Intent intent = makeIntent(UserHandle.USER_CURRENT); IntentReceiver receiver = new IntentReceiver(); diff --git a/cmds/pm/src/com/android/commands/pm/Pm.java b/cmds/pm/src/com/android/commands/pm/Pm.java index 32a8088e9c4e..ace4e3284930 100644 --- a/cmds/pm/src/com/android/commands/pm/Pm.java +++ b/cmds/pm/src/com/android/commands/pm/Pm.java @@ -49,9 +49,12 @@ import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IUserManager; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ResultReceiver; +import android.os.SELinux; import android.os.ServiceManager; +import android.os.ShellCallback; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; @@ -68,6 +71,7 @@ import libcore.io.IoUtils; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -284,13 +288,45 @@ public final class Pm { } } + static final class MyShellCallback extends ShellCallback { + @Override public ParcelFileDescriptor onOpenOutputFile(String path, String seLinuxContext) { + File file = new File(path); + final ParcelFileDescriptor fd; + try { + fd = ParcelFileDescriptor.open(file, + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE | + ParcelFileDescriptor.MODE_WRITE_ONLY); + } catch (FileNotFoundException e) { + String msg = "Unable to open file " + path + ": " + e; + System.err.println(msg); + throw new IllegalArgumentException(msg); + } + if (seLinuxContext != null) { + final String tcon = SELinux.getFileContext(file.getAbsolutePath()); + if (!SELinux.checkSELinuxAccess(seLinuxContext, tcon, "file", "write")) { + try { + fd.close(); + } catch (IOException e) { + } + String msg = "System server has no access to file context " + tcon; + System.err.println(msg + " (from path " + file.getAbsolutePath() + + ", context " + seLinuxContext + ")"); + throw new IllegalArgumentException(msg); + } + } + return fd; + } + } + private int runShellCommand(String serviceName, String[] args) { final HandlerThread handlerThread = new HandlerThread("results"); handlerThread.start(); try { ServiceManager.getService(serviceName).shellCommand( FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, - args, new ResultReceiver(new Handler(handlerThread.getLooper()))); + args, new MyShellCallback(), + new ResultReceiver(new Handler(handlerThread.getLooper()))); return 0; } catch (RemoteException e) { e.printStackTrace(); diff --git a/core/java/android/app/ActivityTransitionState.java b/core/java/android/app/ActivityTransitionState.java index aef1d0c31f9d..60046b5932ee 100644 --- a/core/java/android/app/ActivityTransitionState.java +++ b/core/java/android/app/ActivityTransitionState.java @@ -15,6 +15,7 @@ */ package android.app; +import android.content.Intent; import android.os.Bundle; import android.os.ResultReceiver; import android.transition.Transition; @@ -166,7 +167,11 @@ class ActivityTransitionState { restoreExitedViews(); int result = mEnterActivityOptions.getResultCode(); if (result != 0) { - activity.onActivityReenter(result, mEnterActivityOptions.getResultData()); + Intent intent = mEnterActivityOptions.getResultData(); + if (intent != null) { + intent.setExtrasClassLoader(activity.getClassLoader()); + } + activity.onActivityReenter(result, intent); } } } diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 5a9498f88d5a..191cc4944e26 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -311,9 +311,7 @@ public class AppOpsManager { /** Access APIs for SIP calling over VOIP or WiFi */ public static final String OPSTR_USE_SIP = "android:use_sip"; - /** Access APIs for diverting outgoing calls - * @hide - */ + /** Access APIs for diverting outgoing calls */ public static final String OPSTR_PROCESS_OUTGOING_CALLS = "android:process_outgoing_calls"; /** Use the fingerprint API. */ diff --git a/core/java/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java index 6d6dfebced2c..cd5eff29237c 100644 --- a/core/java/android/bluetooth/BluetoothDevice.java +++ b/core/java/android/bluetooth/BluetoothDevice.java @@ -1596,7 +1596,7 @@ public final class BluetoothDevice implements Parcelable { // BLE is not supported return null; } - BluetoothGatt gatt = new BluetoothGatt(context, iGatt, this, transport); + BluetoothGatt gatt = new BluetoothGatt(iGatt, this, transport); gatt.connect(autoConnect, callback); return gatt; } catch (RemoteException e) {Log.e(TAG, "", e);} diff --git a/core/java/android/bluetooth/BluetoothGatt.java b/core/java/android/bluetooth/BluetoothGatt.java index 17145f2e146a..2bb901221c5b 100644 --- a/core/java/android/bluetooth/BluetoothGatt.java +++ b/core/java/android/bluetooth/BluetoothGatt.java @@ -41,7 +41,6 @@ public final class BluetoothGatt implements BluetoothProfile { private static final boolean DBG = true; private static final boolean VDBG = false; - private final Context mContext; private IBluetoothGatt mService; private BluetoothGattCallback mCallback; private int mClientIf; @@ -492,9 +491,8 @@ public final class BluetoothGatt implements BluetoothProfile { } }; - /*package*/ BluetoothGatt(Context context, IBluetoothGatt iGatt, BluetoothDevice device, + /*package*/ BluetoothGatt(IBluetoothGatt iGatt, BluetoothDevice device, int transport) { - mContext = context; mService = iGatt; mDevice = device; mTransport = transport; diff --git a/core/java/android/bluetooth/BluetoothGattServer.java b/core/java/android/bluetooth/BluetoothGattServer.java index c2bcbb2df636..5ffceba5e11d 100644 --- a/core/java/android/bluetooth/BluetoothGattServer.java +++ b/core/java/android/bluetooth/BluetoothGattServer.java @@ -44,7 +44,6 @@ public final class BluetoothGattServer implements BluetoothProfile { private static final boolean DBG = true; private static final boolean VDBG = false; - private final Context mContext; private BluetoothAdapter mAdapter; private IBluetoothGatt mService; private BluetoothGattServerCallback mCallback; @@ -298,8 +297,7 @@ public final class BluetoothGattServer implements BluetoothProfile { /** * Create a BluetoothGattServer proxy object. */ - /*package*/ BluetoothGattServer(Context context, IBluetoothGatt iGatt, int transport) { - mContext = context; + /*package*/ BluetoothGattServer(IBluetoothGatt iGatt, int transport) { mService = iGatt; mAdapter = BluetoothAdapter.getDefaultAdapter(); mCallback = null; diff --git a/core/java/android/bluetooth/BluetoothManager.java b/core/java/android/bluetooth/BluetoothManager.java index 00058a979094..29283e793ce9 100644 --- a/core/java/android/bluetooth/BluetoothManager.java +++ b/core/java/android/bluetooth/BluetoothManager.java @@ -236,7 +236,7 @@ public final class BluetoothManager { Log.e(TAG, "Fail to get GATT Server connection"); return null; } - BluetoothGattServer mGattServer = new BluetoothGattServer(context, iGatt,transport); + BluetoothGattServer mGattServer = new BluetoothGattServer(iGatt,transport); Boolean regStatus = mGattServer.registerCallback(callback); return regStatus? mGattServer : null; } catch (RemoteException e) { diff --git a/core/java/android/hardware/camera2/DngCreator.java b/core/java/android/hardware/camera2/DngCreator.java index 45fa15e043e5..1a51acd6c5ec 100644 --- a/core/java/android/hardware/camera2/DngCreator.java +++ b/core/java/android/hardware/camera2/DngCreator.java @@ -119,8 +119,14 @@ public final class DngCreator implements AutoCloseable { captureTime = timestamp / 1000000 + timeOffset; } + // Create this fresh each time since the time zone may change while a long-running application + // is active. + final DateFormat dateTimeStampFormat = + new SimpleDateFormat(TIFF_DATETIME_FORMAT); + dateTimeStampFormat.setTimeZone(TimeZone.getDefault()); + // Format for metadata - String formattedCaptureTime = sDateTimeStampFormat.format(captureTime); + String formattedCaptureTime = dateTimeStampFormat.format(captureTime); nativeInit(characteristics.getNativeCopy(), metadata.getNativeCopy(), formattedCaptureTime); @@ -467,13 +473,10 @@ public final class DngCreator implements AutoCloseable { private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd"; private static final String TIFF_DATETIME_FORMAT = "yyyy:MM:dd HH:mm:ss"; private static final DateFormat sExifGPSDateStamp = new SimpleDateFormat(GPS_DATE_FORMAT_STR); - private static final DateFormat sDateTimeStampFormat = - new SimpleDateFormat(TIFF_DATETIME_FORMAT); private final Calendar mGPSTimeStampCalendar = Calendar .getInstance(TimeZone.getTimeZone("UTC")); static { - sDateTimeStampFormat.setTimeZone(TimeZone.getDefault()); sExifGPSDateStamp.setTimeZone(TimeZone.getTimeZone("UTC")); } diff --git a/core/java/android/hardware/usb/UsbDeviceConnection.java b/core/java/android/hardware/usb/UsbDeviceConnection.java index f7bf1e43a010..160e261de530 100644 --- a/core/java/android/hardware/usb/UsbDeviceConnection.java +++ b/core/java/android/hardware/usb/UsbDeviceConnection.java @@ -24,7 +24,7 @@ import android.os.ParcelFileDescriptor; import dalvik.system.CloseGuard; import java.io.FileDescriptor; - +import java.nio.ByteBuffer; /** * This class is used for sending and receiving data and control messages to a USB device. @@ -257,13 +257,20 @@ public class UsbDeviceConnection { /** * Waits for the result of a {@link android.hardware.usb.UsbRequest#queue} operation - * Note that this may return requests queued on multiple - * {@link android.hardware.usb.UsbEndpoint}s. - * When multiple endpoints are in use, {@link android.hardware.usb.UsbRequest#getEndpoint} and - * {@link android.hardware.usb.UsbRequest#getClientData} can be useful in determining - * how to process the result of this function. + * <p>Note that this may return requests queued on multiple + * {@link android.hardware.usb.UsbEndpoint}s. When multiple endpoints are in use, + * {@link android.hardware.usb.UsbRequest#getEndpoint} and {@link + * android.hardware.usb.UsbRequest#getClientData} can be useful in determining how to process + * the result of this function.</p> + * <p>Position and array offset of the request's buffer are ignored and assumed to be 0. The + * position will be set to the number of bytes read/written.</p> * * @return a completed USB request, or null if an error occurred + * + * @throws IllegalArgumentException if the number of bytes read or written is more than the + * limit of the request's buffer. The number of bytes is + * determined by the {@code length} parameter of + * {@link UsbRequest#queue(ByteBuffer, int)} */ public UsbRequest requestWait() { UsbRequest request = native_request_wait(); diff --git a/core/java/android/hardware/usb/UsbRequest.java b/core/java/android/hardware/usb/UsbRequest.java index b531e5fb3db3..9f7592f880e6 100644 --- a/core/java/android/hardware/usb/UsbRequest.java +++ b/core/java/android/hardware/usb/UsbRequest.java @@ -17,6 +17,7 @@ package android.hardware.usb; import android.util.Log; +import com.android.internal.util.Preconditions; import dalvik.system.CloseGuard; import java.nio.ByteBuffer; @@ -66,7 +67,7 @@ public class UsbRequest { */ public boolean initialize(UsbDeviceConnection connection, UsbEndpoint endpoint) { mEndpoint = endpoint; - mConnection = connection; + mConnection = Preconditions.checkNotNull(connection); boolean wasInitialized = native_init(connection, endpoint.getAddress(), endpoint.getAttributes(), endpoint.getMaxPacketSize(), endpoint.getInterval()); @@ -137,15 +138,16 @@ public class UsbRequest { /** * Queues the request to send or receive data on its endpoint. - * For OUT endpoints, the given buffer data will be sent on the endpoint. - * For IN endpoints, the endpoint will attempt to read the given number of bytes - * into the specified buffer. - * If the queueing operation is successful, we return true and the result will be - * returned via {@link android.hardware.usb.UsbDeviceConnection#requestWait} + * <p>For OUT endpoints, the given buffer data will be sent on the endpoint. For IN endpoints, + * the endpoint will attempt to read the given number of bytes into the specified buffer. If the + * queueing operation is successful, we return true and the result will be returned via {@link + * android.hardware.usb.UsbDeviceConnection#requestWait}</p> + * + * @param buffer the buffer containing the bytes to write, or location to store the results of a + * read. Position and array offset will be ignored and assumed to be 0. Limit and + * capacity will be ignored. + * @param length number of bytes to read or write. * - * @param buffer the buffer containing the bytes to write, or location to store - * the results of a read - * @param length number of bytes to read or write * @return true if the queueing operation succeeded */ public boolean queue(ByteBuffer buffer, int length) { @@ -155,6 +157,9 @@ public class UsbRequest { mBuffer = buffer; mLength = length; + // Note: On a buffer slice we lost the capacity information about the underlying buffer, + // hence we cannot check if the access would be a data leak/memory corruption. + boolean result; if (buffer.isDirect()) { result = native_queue_direct(buffer, length, out); diff --git a/core/java/android/net/metrics/ValidationProbeEvent.java b/core/java/android/net/metrics/ValidationProbeEvent.java index 331cf0cdd4ad..1a31b56f1ffb 100644 --- a/core/java/android/net/metrics/ValidationProbeEvent.java +++ b/core/java/android/net/metrics/ValidationProbeEvent.java @@ -34,10 +34,12 @@ import java.lang.annotation.RetentionPolicy; @SystemApi public final class ValidationProbeEvent implements Parcelable { - public static final int PROBE_DNS = 0; - public static final int PROBE_HTTP = 1; - public static final int PROBE_HTTPS = 2; - public static final int PROBE_PAC = 3; + public static final int PROBE_DNS = 0; + public static final int PROBE_HTTP = 1; + public static final int PROBE_HTTPS = 2; + public static final int PROBE_PAC = 3; + /** {@hide} */ + public static final int PROBE_FALLBACK = 4; public static final int DNS_FAILURE = 0; public static final int DNS_SUCCESS = 1; @@ -57,7 +59,7 @@ public final class ValidationProbeEvent implements Parcelable { public final @ProbeType int probeType; public final @ReturnCode int returnCode; - /** @hide */ + /** {@hide} */ public ValidationProbeEvent( int netId, long durationMs, @ProbeType int probeType, @ReturnCode int returnCode) { this.netId = netId; diff --git a/core/java/android/os/AsyncTask.java b/core/java/android/os/AsyncTask.java index 9a4b599f0e9b..fea64ec5ee1e 100644 --- a/core/java/android/os/AsyncTask.java +++ b/core/java/android/os/AsyncTask.java @@ -304,6 +304,9 @@ public abstract class AsyncTask<Params, Progress, Result> { //noinspection unchecked result = doInBackground(mParams); Binder.flushPendingCommands(); + } catch (Throwable tr) { + mCancelled.set(true); + throw tr; } finally { postResult(result); } diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index ea8ba2f1e6df..cf77567540f0 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -23,7 +23,6 @@ import libcore.io.IoUtils; import java.io.FileDescriptor; import java.io.FileOutputStream; -import java.io.IOException; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.lang.reflect.Modifier; @@ -361,13 +360,14 @@ public class Binder implements IBinder { ParcelFileDescriptor out = data.readFileDescriptor(); ParcelFileDescriptor err = data.readFileDescriptor(); String[] args = data.readStringArray(); + ShellCallback shellCallback = ShellCallback.CREATOR.createFromParcel(data); ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(data); try { if (out != null) { shellCommand(in != null ? in.getFileDescriptor() : null, out.getFileDescriptor(), err != null ? err.getFileDescriptor() : out.getFileDescriptor(), - args, resultReceiver); + args, shellCallback, resultReceiver); } } finally { IoUtils.closeQuietly(in); @@ -459,13 +459,15 @@ public class Binder implements IBinder { * @param out The raw file descriptor that normal command messages should be written to. * @param err The raw file descriptor that command error messages should be written to. * @param args Command-line arguments. + * @param callback Callback through which to interact with the invoking shell. * @param resultReceiver Called when the command has finished executing, with the result code. * @throws RemoteException * @hide */ public void shellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, - String[] args, ResultReceiver resultReceiver) throws RemoteException { - onShellCommand(in, out, err, args, resultReceiver); + String[] args, ShellCallback callback, + ResultReceiver resultReceiver) throws RemoteException { + onShellCommand(in, out, err, args, callback, resultReceiver); } /** @@ -477,7 +479,7 @@ public class Binder implements IBinder { * @hide */ public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, - String[] args, ResultReceiver resultReceiver) throws RemoteException { + String[] args, ShellCallback callback, ResultReceiver resultReceiver) throws RemoteException { FileOutputStream fout = new FileOutputStream(err != null ? err : out); PrintWriter pw = new FastPrintWriter(fout); pw.println("No shell command implementation."); @@ -650,13 +652,15 @@ final class BinderProxy implements IBinder { } public void shellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, - String[] args, ResultReceiver resultReceiver) throws RemoteException { + String[] args, ShellCallback callback, + ResultReceiver resultReceiver) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeFileDescriptor(in); data.writeFileDescriptor(out); data.writeFileDescriptor(err); data.writeStringArray(args); + ShellCallback.writeToParcel(callback, data); resultReceiver.writeToParcel(data, 0); try { transact(SHELL_COMMAND_TRANSACTION, data, reply, 0); diff --git a/core/java/android/os/HwBlob.java b/core/java/android/os/HwBlob.java index 153c6e634ecb..88226f0a1665 100644 --- a/core/java/android/os/HwBlob.java +++ b/core/java/android/os/HwBlob.java @@ -16,6 +16,8 @@ package android.os; +import android.annotation.NonNull; + import libcore.util.NativeAllocationRegistry; /** @hide */ @@ -54,6 +56,69 @@ public class HwBlob { public native final long handle(); + public static Boolean[] wrapArray(@NonNull boolean[] array) { + final int n = array.length; + Boolean[] wrappedArray = new Boolean[n]; + for (int i = 0; i < n; ++i) { + wrappedArray[i] = array[i]; + } + return wrappedArray; + } + + public static Long[] wrapArray(@NonNull long[] array) { + final int n = array.length; + Long[] wrappedArray = new Long[n]; + for (int i = 0; i < n; ++i) { + wrappedArray[i] = array[i]; + } + return wrappedArray; + } + + public static Byte[] wrapArray(@NonNull byte[] array) { + final int n = array.length; + Byte[] wrappedArray = new Byte[n]; + for (int i = 0; i < n; ++i) { + wrappedArray[i] = array[i]; + } + return wrappedArray; + } + + public static Short[] wrapArray(@NonNull short[] array) { + final int n = array.length; + Short[] wrappedArray = new Short[n]; + for (int i = 0; i < n; ++i) { + wrappedArray[i] = array[i]; + } + return wrappedArray; + } + + public static Integer[] wrapArray(@NonNull int[] array) { + final int n = array.length; + Integer[] wrappedArray = new Integer[n]; + for (int i = 0; i < n; ++i) { + wrappedArray[i] = array[i]; + } + return wrappedArray; + } + + public static Float[] wrapArray(@NonNull float[] array) { + final int n = array.length; + Float[] wrappedArray = new Float[n]; + for (int i = 0; i < n; ++i) { + wrappedArray[i] = array[i]; + } + return wrappedArray; + } + + public static Double[] wrapArray(@NonNull double[] array) { + final int n = array.length; + Double[] wrappedArray = new Double[n]; + for (int i = 0; i < n; ++i) { + wrappedArray[i] = array[i]; + } + return wrappedArray; + } + // Returns address of the "freeFunction". private static native final long native_init(); diff --git a/core/java/android/os/IBinder.java b/core/java/android/os/IBinder.java index 0fa87509a77a..f762a052cb41 100644 --- a/core/java/android/os/IBinder.java +++ b/core/java/android/os/IBinder.java @@ -220,11 +220,13 @@ public interface IBinder { * @param out The raw file descriptor that normal command messages should be written to. * @param err The raw file descriptor that command error messages should be written to. * @param args Command-line arguments. + * @param shellCallback Optional callback to the caller's shell to perform operations in it. * @param resultReceiver Called when the command has finished executing, with the result code. * @hide */ public void shellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, - String[] args, ResultReceiver resultReceiver) throws RemoteException; + String[] args, ShellCallback shellCallback, + ResultReceiver resultReceiver) throws RemoteException; /** * Perform a generic operation with the object. diff --git a/core/java/android/os/IRecoverySystem.aidl b/core/java/android/os/IRecoverySystem.aidl index 12830a4996aa..c5ceecd7c8b9 100644 --- a/core/java/android/os/IRecoverySystem.aidl +++ b/core/java/android/os/IRecoverySystem.aidl @@ -25,4 +25,5 @@ interface IRecoverySystem { boolean uncrypt(in String packageFile, IRecoverySystemProgressListener listener); boolean setupBcb(in String command); boolean clearBcb(); + void rebootRecoveryWithCommand(in String command); } diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index 90bd11fe83bc..d48431afe691 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -93,6 +93,14 @@ public class RecoverySystem { */ public static final File UNCRYPT_PACKAGE_FILE = new File(RECOVERY_DIR, "uncrypt_file"); + /** + * UNCRYPT_STATUS_FILE stores the time cost (and error code in the case of a failure) + * of uncrypt. + * + * @hide + */ + public static final File UNCRYPT_STATUS_FILE = new File(RECOVERY_DIR, "uncrypt_status"); + // Length limits for reading files. private static final int LOG_FILE_MAX_LENGTH = 64 * 1024; @@ -692,28 +700,22 @@ public class RecoverySystem { * @throws IOException if something goes wrong. */ private static void bootCommand(Context context, String... args) throws IOException { - synchronized (sRequestLock) { - LOG_FILE.delete(); + LOG_FILE.delete(); - StringBuilder command = new StringBuilder(); - for (String arg : args) { - if (!TextUtils.isEmpty(arg)) { - command.append(arg); - command.append("\n"); - } + StringBuilder command = new StringBuilder(); + for (String arg : args) { + if (!TextUtils.isEmpty(arg)) { + command.append(arg); + command.append("\n"); } + } - // Write the command into BCB (bootloader control block). - RecoverySystem rs = (RecoverySystem) context.getSystemService( - Context.RECOVERY_SERVICE); - rs.setupBcb(command.toString()); - - // Having set up the BCB, go ahead and reboot. - PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - pm.reboot(PowerManager.REBOOT_RECOVERY); + // Write the command into BCB (bootloader control block) and boot from + // there. Will not return unless failed. + RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE); + rs.rebootRecoveryWithCommand(command.toString()); - throw new IOException("Reboot failed (no permissions?)"); - } + throw new IOException("Reboot failed (no permissions?)"); } // Read last_install; then report time (in seconds) and I/O (in MiB) for @@ -908,6 +910,17 @@ public class RecoverySystem { } /** + * Talks to RecoverySystemService via Binder to set up the BCB command and + * reboot into recovery accordingly. + */ + private void rebootRecoveryWithCommand(String command) { + try { + mService.rebootRecoveryWithCommand(command); + } catch (RemoteException ignored) { + } + } + + /** * Internally, recovery treats each line of the command file as a separate * argv, so we only need to protect against newlines and nulls. */ diff --git a/core/java/android/os/ShellCallback.java b/core/java/android/os/ShellCallback.java new file mode 100644 index 000000000000..e7fe697f9c54 --- /dev/null +++ b/core/java/android/os/ShellCallback.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.util.Log; + +import com.android.internal.os.IShellCallback; + +/** + * Special-purpose API for use with {@link IBinder#shellCommand IBinder.shellCommand} for + * performing operations back on the invoking shell. + * @hide + */ +public class ShellCallback implements Parcelable { + final static String TAG = "ShellCallback"; + + final static boolean DEBUG = false; + + final boolean mLocal; + + IShellCallback mShellCallback; + + class MyShellCallback extends IShellCallback.Stub { + public ParcelFileDescriptor openOutputFile(String path, String seLinuxContext) { + return onOpenOutputFile(path, seLinuxContext); + } + } + + /** + * Create a new ShellCallback to receive requests. + */ + public ShellCallback() { + mLocal = true; + } + + /** + * Ask the shell to open a file for writing. This will truncate the file if it + * already exists. It will create the file if it doesn't exist. + * @param path Path of the file to be opened/created. + * @param seLinuxContext Optional SELinux context that must be allowed to have + * access to the file; if null, nothing is required. + */ + public ParcelFileDescriptor openOutputFile(String path, String seLinuxContext) { + if (DEBUG) Log.d(TAG, "openOutputFile " + this + ": mLocal=" + mLocal + + " mShellCallback=" + mShellCallback); + + if (mLocal) { + return onOpenOutputFile(path, seLinuxContext); + } + + if (mShellCallback != null) { + try { + return mShellCallback.openOutputFile(path, seLinuxContext); + } catch (RemoteException e) { + Log.w(TAG, "Failure opening " + path, e); + } + } + return null; + } + + public ParcelFileDescriptor onOpenOutputFile(String path, String seLinuxContext) { + return null; + } + + public static void writeToParcel(ShellCallback callback, Parcel out) { + if (callback == null) { + out.writeStrongBinder(null); + } else { + callback.writeToParcel(out, 0); + } + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + synchronized (this) { + if (mShellCallback == null) { + mShellCallback = new MyShellCallback(); + } + out.writeStrongBinder(mShellCallback.asBinder()); + } + } + + ShellCallback(Parcel in) { + mLocal = false; + mShellCallback = IShellCallback.Stub.asInterface(in.readStrongBinder()); + } + + public static final Parcelable.Creator<ShellCallback> CREATOR + = new Parcelable.Creator<ShellCallback>() { + public ShellCallback createFromParcel(Parcel in) { + return new ShellCallback(in); + } + public ShellCallback[] newArray(int size) { + return new ShellCallback[size]; + } + }; +} diff --git a/core/java/android/os/ShellCommand.java b/core/java/android/os/ShellCommand.java index fc804e592148..831c9b27ac45 100644 --- a/core/java/android/os/ShellCommand.java +++ b/core/java/android/os/ShellCommand.java @@ -40,6 +40,7 @@ public abstract class ShellCommand { private FileDescriptor mOut; private FileDescriptor mErr; private String[] mArgs; + private ShellCallback mShellCallback; private ResultReceiver mResultReceiver; private String mCmd; @@ -55,12 +56,13 @@ public abstract class ShellCommand { private InputStream mInputStream; public void init(Binder target, FileDescriptor in, FileDescriptor out, FileDescriptor err, - String[] args, int firstArgPos) { + String[] args, ShellCallback callback, int firstArgPos) { mTarget = target; mIn = in; mOut = out; mErr = err; mArgs = args; + mShellCallback = callback; mResultReceiver = null; mCmd = null; mArgPos = firstArgPos; @@ -74,7 +76,7 @@ public abstract class ShellCommand { } public int exec(Binder target, FileDescriptor in, FileDescriptor out, FileDescriptor err, - String[] args, ResultReceiver resultReceiver) { + String[] args, ShellCallback callback, ResultReceiver resultReceiver) { String cmd; int start; if (args != null && args.length > 0) { @@ -84,7 +86,7 @@ public abstract class ShellCommand { cmd = null; start = 0; } - init(target, in, out, err, args, start); + init(target, in, out, err, args, callback, start); mCmd = cmd; mResultReceiver = resultReceiver; @@ -105,7 +107,7 @@ public abstract class ShellCommand { // go. PrintWriter eout = getErrPrintWriter(); eout.println(); - eout.println("Exception occurred while dumping:"); + eout.println("Exception occurred while executing:"); e.printStackTrace(eout); } finally { if (DEBUG) Slog.d(TAG, "Flushing output streams on " + mTarget); @@ -257,6 +259,13 @@ public abstract class ShellCommand { return arg; } + /** + * Return the {@link ShellCallback} for communicating back with the calling shell. + */ + public ShellCallback getShellCallback() { + return mShellCallback; + } + public int handleDefaultCommands(String cmd) { if ("dump".equals(cmd)) { String[] newArgs = new String[mArgs.length-1]; diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index e62440aef04e..bf73089e48e5 100755 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -8067,12 +8067,37 @@ public final class Settings { /** * The server used for captive portal detection upon a new conection. A * 204 response code from the server is used for validation. + * TODO: remove this deprecated symbol. * * @hide */ public static final String CAPTIVE_PORTAL_SERVER = "captive_portal_server"; /** + * The URL used for HTTPS captive portal detection upon a new connection. + * A 204 response code from the server is used for validation. + * + * @hide + */ + public static final String CAPTIVE_PORTAL_HTTPS_URL = "captive_portal_https_url"; + + /** + * The URL used for HTTP captive portal detection upon a new connection. + * A 204 response code from the server is used for validation. + * + * @hide + */ + public static final String CAPTIVE_PORTAL_HTTP_URL = "captive_portal_http_url"; + + /** + * The URL used for fallback HTTP captive portal detection when previous HTTP + * and HTTPS captive portal detection attemps did not return a conclusive answer. + * + * @hide + */ + public static final String CAPTIVE_PORTAL_FALLBACK_URL = "captive_portal_fallback_url"; + + /** * Whether to use HTTPS for network validation. This is enabled by default and the setting * needs to be set to 0 to disable it. This setting is a misnomer because captive portals * don't actually use HTTPS, but it's consistent with the other settings. @@ -8082,6 +8107,14 @@ public final class Settings { public static final String CAPTIVE_PORTAL_USE_HTTPS = "captive_portal_use_https"; /** + * Which User-Agent string to use in the header of the captive portal detection probes. + * The User-Agent field is unset when this setting has no value (HttpUrlConnection default). + * + * @hide + */ + public static final String CAPTIVE_PORTAL_USER_AGENT = "captive_portal_user_agent"; + + /** * Whether network service discovery is enabled. * * @hide @@ -8792,7 +8825,7 @@ public final class Settings { public static final String WFC_IMS_ENABLED = "wfc_ims_enabled"; /** - * WFC Mode. + * WFC mode on home/non-roaming network. * <p> * Type: int - 2=Wi-Fi preferred, 1=Cellular preferred, 0=Wi-Fi only * @@ -8801,6 +8834,15 @@ public final class Settings { public static final String WFC_IMS_MODE = "wfc_ims_mode"; /** + * WFC mode on roaming network. + * <p> + * Type: int - see {@link WFC_IMS_MODE} for values + * + * @hide + */ + public static final String WFC_IMS_ROAMING_MODE = "wfc_ims_roaming_mode"; + + /** * Whether WFC roaming is enabled * <p> * Type: int (0 for false, 1 for true) @@ -8926,6 +8968,14 @@ public final class Settings { public static final String DATABASE_DOWNGRADE_REASON = "database_downgrade_reason"; /** + * Flag to toggle journal mode WAL on or off for the contacts database. WAL is enabled by + * default. Set to 0 to disable. + * + * @hide + */ + public static final String CONTACTS_DATABASE_WAL_ENABLED = "contacts_database_wal_enabled"; + + /** * Settings to backup. This is here so that it's in the same place as the settings * keys and easy to update. * diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 2a2f659c28f8..816bcf09a89d 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -600,7 +600,9 @@ public class SurfaceView extends View { // surfaceDestroyed and surfaceCreated, we force a disconnect, // so the next connect will always work if we end up reusing // the surface. - mSurface.forceScopedDisconnect(); + if (mSurface.isValid()) { + mSurface.forceScopedDisconnect(); + } } } diff --git a/core/java/com/android/internal/os/BaseCommand.java b/core/java/com/android/internal/os/BaseCommand.java index c067da7e1dec..3baccee049b0 100644 --- a/core/java/com/android/internal/os/BaseCommand.java +++ b/core/java/com/android/internal/os/BaseCommand.java @@ -36,6 +36,8 @@ public abstract class BaseCommand { public static final String NO_SYSTEM_ERROR_CODE = "Error type 2"; public static final String NO_CLASS_ERROR_CODE = "Error type 3"; + private String[] mRawArgs; + /** * Call to run the command. */ @@ -45,7 +47,8 @@ public abstract class BaseCommand { return; } - mArgs.init(null, null, null, null, args, 0); + mRawArgs = args; + mArgs.init(null, null, null, null, args, null, 0); try { onRun(); @@ -109,4 +112,11 @@ public abstract class BaseCommand { public String nextArgRequired() { return mArgs.getNextArgRequired(); } + + /** + * Return the original raw argument list supplied to the command. + */ + public String[] getRawArgs() { + return mRawArgs; + } } diff --git a/core/java/com/android/internal/os/IShellCallback.aidl b/core/java/com/android/internal/os/IShellCallback.aidl new file mode 100644 index 000000000000..57d67890d840 --- /dev/null +++ b/core/java/com/android/internal/os/IShellCallback.aidl @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2016, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.os; + +import android.os.ParcelFileDescriptor; + +/** @hide */ +interface IShellCallback { + ParcelFileDescriptor openOutputFile(String path, String seLinuxContext); +} diff --git a/core/java/com/android/internal/policy/BackdropFrameRenderer.java b/core/java/com/android/internal/policy/BackdropFrameRenderer.java index 619303f34c32..1abb59b006dd 100644 --- a/core/java/com/android/internal/policy/BackdropFrameRenderer.java +++ b/core/java/com/android/internal/policy/BackdropFrameRenderer.java @@ -110,12 +110,15 @@ public class BackdropFrameRenderer extends Thread implements Choreographer.Frame int statusBarColor, int navigationBarColor) { mDecorView = decorView; mResizingBackgroundDrawable = resizingBackgroundDrawable != null + && resizingBackgroundDrawable.getConstantState() != null ? resizingBackgroundDrawable.getConstantState().newDrawable() : null; mCaptionBackgroundDrawable = captionBackgroundDrawableDrawable != null + && captionBackgroundDrawableDrawable.getConstantState() != null ? captionBackgroundDrawableDrawable.getConstantState().newDrawable() : null; mUserCaptionBackgroundDrawable = userCaptionBackgroundDrawable != null + && userCaptionBackgroundDrawable.getConstantState() != null ? userCaptionBackgroundDrawable.getConstantState().newDrawable() : null; if (mCaptionBackgroundDrawable == null) { diff --git a/core/java/com/android/internal/view/FloatingActionMode.java b/core/java/com/android/internal/view/FloatingActionMode.java index 1203dd2c7217..effe21902f91 100644 --- a/core/java/com/android/internal/view/FloatingActionMode.java +++ b/core/java/com/android/internal/view/FloatingActionMode.java @@ -59,15 +59,19 @@ public class FloatingActionMode extends ActionMode { private final Runnable mMovingOff = new Runnable() { public void run() { - mFloatingToolbarVisibilityHelper.setMoving(false); - mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); + if (isViewStillActive()) { + mFloatingToolbarVisibilityHelper.setMoving(false); + mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); + } } }; private final Runnable mHideOff = new Runnable() { public void run() { - mFloatingToolbarVisibilityHelper.setHideRequested(false); - mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); + if (isViewStillActive()) { + mFloatingToolbarVisibilityHelper.setHideRequested(false); + mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); + } } }; @@ -301,6 +305,11 @@ public class FloatingActionMode extends ActionMode { mOriginatingView.removeCallbacks(mHideOff); } + private boolean isViewStillActive() { + return mOriginatingView.getWindowVisibility() == View.VISIBLE + && mOriginatingView.isShown(); + } + /** * A helper for showing/hiding the floating toolbar depending on certain states. */ diff --git a/core/jni/android/graphics/Bitmap.cpp b/core/jni/android/graphics/Bitmap.cpp index 18c4ee3efdee..6acb76d365b5 100755 --- a/core/jni/android/graphics/Bitmap.cpp +++ b/core/jni/android/graphics/Bitmap.cpp @@ -127,12 +127,10 @@ private: } }; -Bitmap::Bitmap(JNIEnv* env, jbyteArray storageObj, void* address, - const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable) - : mPixelStorageType(PixelStorageType::Java) { - env->GetJavaVM(&mPixelStorage.java.jvm); - mPixelStorage.java.jweakRef = env->NewWeakGlobalRef(storageObj); - mPixelStorage.java.jstrongRef = nullptr; +Bitmap::Bitmap(void* address, size_t size, const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable) + : mPixelStorageType(PixelStorageType::Heap) { + mPixelStorage.heap.address = address; + mPixelStorage.heap.size = size; mPixelRef.reset(new WrappedPixelRef(this, address, info, rowBytes, ctable)); // Note: this will trigger a call to onStrongRefDestroyed(), but // we want the pixel ref to have a ref count of 0 at this point @@ -187,12 +185,8 @@ void Bitmap::doFreePixels() { munmap(mPixelStorage.ashmem.address, mPixelStorage.ashmem.size); close(mPixelStorage.ashmem.fd); break; - case PixelStorageType::Java: - JNIEnv* env = jniEnv(); - LOG_ALWAYS_FATAL_IF(mPixelStorage.java.jstrongRef, - "Deleting a bitmap wrapper while there are outstanding strong " - "references! mPinnedRefCount = %d", mPinnedRefCount); - env->DeleteWeakGlobalRef(mPixelStorage.java.jweakRef); + case PixelStorageType::Heap: + free(mPixelStorage.heap.address); break; } @@ -219,6 +213,15 @@ int Bitmap::getAshmemFd() const { } } +size_t Bitmap::getAllocationByteCount() const { + switch (mPixelStorageType) { + case PixelStorageType::Heap: + return mPixelStorage.heap.size; + default: + return rowBytes() * height(); + } +} + const SkImageInfo& Bitmap::info() const { return mPixelRef->info(); } @@ -244,7 +247,6 @@ SkPixelRef* Bitmap::refPixelRefLocked() { // We just restored this from 0, pin the pixels and inc the strong count // Note that there *might be* an incoming onStrongRefDestroyed from whatever // last unref'd - pinPixelsLocked(); mPinnedRefCount++; } return mPixelRef.get(); @@ -283,13 +285,6 @@ bool Bitmap::shouldDisposeSelfLocked() { return mPinnedRefCount == 0 && !mAttachedToJava; } -JNIEnv* Bitmap::jniEnv() { - JNIEnv* env; - auto success = mPixelStorage.java.jvm->GetEnv((void**)&env, JNI_VERSION_1_6); - LOG_ALWAYS_FATAL_IF(success != JNI_OK, - "Failed to get JNIEnv* from JVM: %p", mPixelStorage.java.jvm); - return env; -} void Bitmap::onStrongRefDestroyed() { bool disposeSelf = false; @@ -298,7 +293,6 @@ void Bitmap::onStrongRefDestroyed() { if (mPinnedRefCount > 0) { mPinnedRefCount--; if (mPinnedRefCount == 0) { - unpinPixelsLocked(); disposeSelf = shouldDisposeSelfLocked(); } } @@ -308,49 +302,6 @@ void Bitmap::onStrongRefDestroyed() { } } -void Bitmap::pinPixelsLocked() { - switch (mPixelStorageType) { - case PixelStorageType::Invalid: - LOG_ALWAYS_FATAL("Cannot pin invalid pixels!"); - break; - case PixelStorageType::External: - case PixelStorageType::Ashmem: - // Nothing to do - break; - case PixelStorageType::Java: { - JNIEnv* env = jniEnv(); - if (!mPixelStorage.java.jstrongRef) { - mPixelStorage.java.jstrongRef = reinterpret_cast<jbyteArray>( - env->NewGlobalRef(mPixelStorage.java.jweakRef)); - if (!mPixelStorage.java.jstrongRef) { - LOG_ALWAYS_FATAL("Failed to acquire strong reference to pixels"); - } - } - break; - } - } -} - -void Bitmap::unpinPixelsLocked() { - switch (mPixelStorageType) { - case PixelStorageType::Invalid: - LOG_ALWAYS_FATAL("Cannot unpin invalid pixels!"); - break; - case PixelStorageType::External: - case PixelStorageType::Ashmem: - // Don't need to do anything - break; - case PixelStorageType::Java: { - JNIEnv* env = jniEnv(); - if (mPixelStorage.java.jstrongRef) { - env->DeleteGlobalRef(mPixelStorage.java.jstrongRef); - mPixelStorage.java.jstrongRef = nullptr; - } - break; - } - } -} - void Bitmap::getSkBitmap(SkBitmap* outBitmap) { assertValid(); android::AutoMutex _lock(mLock); @@ -723,7 +674,7 @@ static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors, SkBitmap bitmap; bitmap.setInfo(SkImageInfo::Make(width, height, colorType, kPremul_SkAlphaType)); - Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, &bitmap, NULL); + Bitmap* nativeBitmap = GraphicsJNI::allocateHeapPixelRef(&bitmap, NULL); if (!nativeBitmap) { return NULL; } @@ -742,8 +693,8 @@ static jobject Bitmap_copy(JNIEnv* env, jobject, jlong srcHandle, SkBitmap src; reinterpret_cast<Bitmap*>(srcHandle)->getSkBitmap(&src); SkColorType dstCT = GraphicsJNI::legacyBitmapConfigToColorType(dstConfigHandle); - SkBitmap result; - JavaPixelAllocator allocator(env); + SkBitmap result; + HeapAllocator allocator; if (!src.copyTo(&result, dstCT, &allocator)) { return NULL; @@ -798,8 +749,7 @@ static jboolean Bitmap_recycle(JNIEnv* env, jobject, jlong bitmapHandle) { } static void Bitmap_reconfigure(JNIEnv* env, jobject clazz, jlong bitmapHandle, - jint width, jint height, jint configHandle, jint allocSize, - jboolean requestPremul) { + jint width, jint height, jint configHandle, jboolean requestPremul) { LocalScopedBitmap bitmap(bitmapHandle); SkColorType colorType = GraphicsJNI::legacyBitmapConfigToColorType(configHandle); @@ -807,8 +757,8 @@ static void Bitmap_reconfigure(JNIEnv* env, jobject clazz, jlong bitmapHandle, if (colorType == kARGB_4444_SkColorType) { colorType = kN32_SkColorType; } - - if (width * height * SkColorTypeBytesPerPixel(colorType) > allocSize) { + size_t requestedSize = width * height * SkColorTypeBytesPerPixel(colorType); + if (requestedSize > bitmap->getAllocationByteCount()) { // done in native as there's no way to get BytesPerPixel in Java doThrowIAE(env, "Bitmap not large enough to support new configuration"); return; @@ -1053,7 +1003,7 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) { #endif // Copy the pixels into a new buffer. - nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, bitmap.get(), ctable); + nativeBitmap = GraphicsJNI::allocateHeapPixelRef(bitmap.get(), ctable); SkSafeUnref(ctable); if (!nativeBitmap) { blob.release(); @@ -1165,7 +1115,7 @@ static jobject Bitmap_extractAlpha(JNIEnv* env, jobject clazz, const android::Paint* paint = reinterpret_cast<android::Paint*>(paintHandle); SkIPoint offset; SkBitmap dst; - JavaPixelAllocator allocator(env); + HeapAllocator allocator; src.extractAlpha(&dst, paint, &allocator, &offset); // If Skia can't allocate pixels for destination bitmap, it resets @@ -1370,6 +1320,11 @@ static void Bitmap_prepareToDraw(JNIEnv* env, jobject, jlong bitmapPtr) { android::uirenderer::renderthread::RenderProxy::prepareToDraw(bitmap); } +static jint Bitmap_getAllocationByteCount(JNIEnv* env, jobject, jlong bitmapPtr) { + LocalScopedBitmap bitmapHandle(bitmapPtr); + return static_cast<jint>(bitmapHandle->getAllocationByteCount()); +} + /////////////////////////////////////////////////////////////////////////////// static const JNINativeMethod gBitmapMethods[] = { @@ -1383,7 +1338,7 @@ static const JNINativeMethod gBitmapMethods[] = { (void*)Bitmap_copyAshmemConfig }, { "nativeGetNativeFinalizer", "()J", (void*)Bitmap_getNativeFinalizer }, { "nativeRecycle", "(J)Z", (void*)Bitmap_recycle }, - { "nativeReconfigure", "(JIIIIZ)V", (void*)Bitmap_reconfigure }, + { "nativeReconfigure", "(JIIIZ)V", (void*)Bitmap_reconfigure }, { "nativeCompress", "(JIILjava/io/OutputStream;[B)Z", (void*)Bitmap_compress }, { "nativeErase", "(JI)V", (void*)Bitmap_erase }, @@ -1414,6 +1369,7 @@ static const JNINativeMethod gBitmapMethods[] = { { "nativeSameAs", "(JJ)Z", (void*)Bitmap_sameAs }, { "nativeRefPixelRef", "(J)J", (void*)Bitmap_refPixelRef }, { "nativePrepareToDraw", "(J)V", (void*)Bitmap_prepareToDraw }, + { "nativeGetAllocationByteCount", "(J)I", (void*)Bitmap_getAllocationByteCount }, }; int register_android_graphics_Bitmap(JNIEnv* env) diff --git a/core/jni/android/graphics/Bitmap.h b/core/jni/android/graphics/Bitmap.h index aaea178e7387..9ae1f3f3d07b 100644 --- a/core/jni/android/graphics/Bitmap.h +++ b/core/jni/android/graphics/Bitmap.h @@ -28,7 +28,7 @@ namespace android { enum class PixelStorageType { Invalid, External, - Java, + Heap, Ashmem, }; @@ -47,8 +47,8 @@ typedef void (*FreeFunc)(void* addr, void* context); */ class Bitmap { public: - Bitmap(JNIEnv* env, jbyteArray storageObj, void* address, - const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable); + Bitmap(void* address, size_t allocSize, const SkImageInfo& info, size_t rowBytes, + SkColorTable* ctable); Bitmap(void* address, void* context, FreeFunc freeFunc, const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable); Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, @@ -56,12 +56,6 @@ public: const SkImageInfo& info() const; - // Returns nullptr if it is not backed by a jbyteArray - jbyteArray javaByteArray() const { - return mPixelStorageType == PixelStorageType::Java - ? mPixelStorage.java.jstrongRef : nullptr; - } - int width() const { return info().width(); } int height() const { return info().height(); } size_t rowBytes() const; @@ -81,6 +75,7 @@ public: bool hasHardwareMipMap(); void setHasHardwareMipMap(bool hasMipMap); int getAshmemFd() const; + size_t getAllocationByteCount() const; private: friend class WrappedPixelRef; @@ -90,8 +85,6 @@ private: void onStrongRefDestroyed(); void pinPixelsLocked(); - void unpinPixelsLocked(); - JNIEnv* jniEnv(); bool shouldDisposeSelfLocked(); void assertValid() const; SkPixelRef* refPixelRefLocked(); @@ -114,10 +107,9 @@ private: size_t size; } ashmem; struct { - JavaVM* jvm; - jweak jweakRef; - jbyteArray jstrongRef; - } java; + void* address; + size_t size; + } heap; } mPixelStorage; }; diff --git a/core/jni/android/graphics/BitmapFactory.cpp b/core/jni/android/graphics/BitmapFactory.cpp index 77799d66f4e0..5a540ce10662 100644 --- a/core/jni/android/graphics/BitmapFactory.cpp +++ b/core/jni/android/graphics/BitmapFactory.cpp @@ -193,7 +193,7 @@ public: bitmap->setPixelRef(mBitmap->refPixelRef())->unref(); // since we're already allocated, we lockPixels right away - // HeapAllocator/JavaPixelAllocator behaves this way too + // HeapAllocator behaves this way too bitmap->lockPixels(); return true; } @@ -339,7 +339,7 @@ static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding } } - JavaPixelAllocator javaAllocator(env); + HeapAllocator defaultAllocator; RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize); ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize); SkBitmap::HeapAllocator heapAllocator; @@ -353,10 +353,10 @@ static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding decodeAllocator = &recyclingAllocator; } else if (willScale) { // This will allocate pixels using a HeapAllocator, since there will be an extra - // scaling step that copies these pixels into Java memory. + // scaling step. decodeAllocator = &heapAllocator; } else { - decodeAllocator = &javaAllocator; + decodeAllocator = &defaultAllocator; } // Set the decode colorType. This is necessary because we can't always support @@ -412,7 +412,7 @@ static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding // Use SkAndroidCodec to perform the decode. SkAndroidCodec::AndroidOptions codecOptions; - codecOptions.fZeroInitialized = (decodeAllocator == &javaAllocator) ? + codecOptions.fZeroInitialized = decodeAllocator == &defaultAllocator ? SkCodec::kYes_ZeroInitialized : SkCodec::kNo_ZeroInitialized; codecOptions.fColorPtr = colorPtr; codecOptions.fColorCount = colorCount; @@ -477,7 +477,7 @@ static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding if (javaBitmap != nullptr) { outputAllocator = &recyclingAllocator; } else { - outputAllocator = &javaAllocator; + outputAllocator = &defaultAllocator; } SkColorType scaledColorType = colorTypeForScaledOutput(decodingBitmap.colorType()); @@ -540,7 +540,7 @@ static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding if (isPremultiplied) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Premultiplied; // now create the java bitmap - return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(), + return GraphicsJNI::createBitmap(env, defaultAllocator.getStorageObjAndReset(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1); } diff --git a/core/jni/android/graphics/BitmapRegionDecoder.cpp b/core/jni/android/graphics/BitmapRegionDecoder.cpp index 970001a123f8..21850bde83d2 100644 --- a/core/jni/android/graphics/BitmapRegionDecoder.cpp +++ b/core/jni/android/graphics/BitmapRegionDecoder.cpp @@ -161,13 +161,13 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in // Set up the pixel allocator SkBRDAllocator* allocator = nullptr; RecyclingClippingPixelAllocator recycleAlloc(recycledBitmap, recycledBytes); - JavaPixelAllocator javaAlloc(env); + HeapAllocator heapAlloc; if (javaBitmap) { allocator = &recycleAlloc; // We are required to match the color type of the recycled bitmap. colorType = recycledBitmap->info().colorType(); } else { - allocator = &javaAlloc; + allocator = &heapAlloc; } // Decode the region. @@ -200,7 +200,7 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in if (!requireUnpremul) { bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Premultiplied; } - return GraphicsJNI::createBitmap(env, javaAlloc.getStorageObjAndReset(), bitmapCreateFlags); + return GraphicsJNI::createBitmap(env, heapAlloc.getStorageObjAndReset(), bitmapCreateFlags); } static jint nativeGetHeight(JNIEnv* env, jobject, jlong brdHandle) { diff --git a/core/jni/android/graphics/Graphics.cpp b/core/jni/android/graphics/Graphics.cpp index c6a51e8ba658..8cee814fa162 100644 --- a/core/jni/android/graphics/Graphics.cpp +++ b/core/jni/android/graphics/Graphics.cpp @@ -416,9 +416,8 @@ jobject GraphicsJNI::createBitmap(JNIEnv* env, android::Bitmap* bitmap, assert_premultiplied(bitmap->info(), isPremultiplied); jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID, - reinterpret_cast<jlong>(bitmap), bitmap->javaByteArray(), - bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied, - ninePatchChunk, ninePatchInsets); + reinterpret_cast<jlong>(bitmap), bitmap->width(), bitmap->height(), density, isMutable, + isPremultiplied, ninePatchChunk, ninePatchInsets); hasException(env); // For the side effect of logging. return obj; } @@ -483,37 +482,28 @@ static bool computeAllocationSize(const SkBitmap& bitmap, size_t* size) { return true; } -android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap, - SkColorTable* ctable) { +android::Bitmap* GraphicsJNI::allocateHeapPixelRef(SkBitmap* bitmap, SkColorTable* ctable) { const SkImageInfo& info = bitmap->info(); if (info.colorType() == kUnknown_SkColorType) { - doThrowIAE(env, "unknown bitmap configuration"); - return NULL; + LOG_ALWAYS_FATAL("unknown bitmap configuration"); + return nullptr; } size_t size; if (!computeAllocationSize(*bitmap, &size)) { - return NULL; + return nullptr; } // we must respect the rowBytes value already set on the bitmap instead of // attempting to compute our own. const size_t rowBytes = bitmap->rowBytes(); - jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime, - gVMRuntime_newNonMovableArray, - gByte_class, size); - if (env->ExceptionCheck() != 0) { - return NULL; - } - SkASSERT(arrayObj); - jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj); - if (env->ExceptionCheck() != 0) { - return NULL; + void* addr = calloc(size, 1); + if (!addr) { + return nullptr; } - SkASSERT(addr); - android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr, - info, rowBytes, ctable); + + android::Bitmap* wrapper = new android::Bitmap(addr, size, info, rowBytes, ctable); wrapper->getSkBitmap(bitmap); // since we're already allocated, we lockPixels right away // HeapAllocator behaves this way too @@ -658,21 +648,16 @@ android::Bitmap* GraphicsJNI::mapAshmemPixelRef(JNIEnv* env, SkBitmap* bitmap, /////////////////////////////////////////////////////////////////////////////// -JavaPixelAllocator::JavaPixelAllocator(JNIEnv* env) { - LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&mJavaVM) != JNI_OK, - "env->GetJavaVM failed"); -} +HeapAllocator::HeapAllocator() {} -JavaPixelAllocator::~JavaPixelAllocator() { +HeapAllocator::~HeapAllocator() { if (mStorage) { mStorage->detachFromJava(); } } -bool JavaPixelAllocator::allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) { - JNIEnv* env = vm2env(mJavaVM); - - mStorage = GraphicsJNI::allocateJavaPixelRef(env, bitmap, ctable); +bool HeapAllocator::allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) { + mStorage = GraphicsJNI::allocateHeapPixelRef(bitmap, ctable); return mStorage != nullptr; } @@ -830,7 +815,7 @@ int register_android_graphics_Graphics(JNIEnv* env) gBitmap_class = make_globalref(env, "android/graphics/Bitmap"); gBitmap_nativePtr = getFieldIDCheck(env, gBitmap_class, "mNativePtr", "J"); - gBitmap_constructorMethodID = env->GetMethodID(gBitmap_class, "<init>", "(J[BIIIZZ[BLandroid/graphics/NinePatch$InsetStruct;)V"); + gBitmap_constructorMethodID = env->GetMethodID(gBitmap_class, "<init>", "(JIIIZZ[BLandroid/graphics/NinePatch$InsetStruct;)V"); gBitmap_reinitMethodID = env->GetMethodID(gBitmap_class, "reinit", "(IIZ)V"); gBitmap_getAllocationByteCountMethodID = env->GetMethodID(gBitmap_class, "getAllocationByteCount", "()I"); gBitmapRegionDecoder_class = make_globalref(env, "android/graphics/BitmapRegionDecoder"); diff --git a/core/jni/android/graphics/GraphicsJNI.h b/core/jni/android/graphics/GraphicsJNI.h index 89636dbfe682..738ad54b715d 100644 --- a/core/jni/android/graphics/GraphicsJNI.h +++ b/core/jni/android/graphics/GraphicsJNI.h @@ -93,8 +93,7 @@ public: static jobject createBitmapRegionDecoder(JNIEnv* env, SkBitmapRegionDecoder* bitmap); - static android::Bitmap* allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap, - SkColorTable* ctable); + static android::Bitmap* allocateHeapPixelRef(SkBitmap* bitmap, SkColorTable* ctable); static android::Bitmap* allocateAshmemPixelRef(JNIEnv* env, SkBitmap* bitmap, SkColorTable* ctable); @@ -119,15 +118,10 @@ public: const SkBitmap& dstBitmap); }; -/** Allocator which allocates the backing buffer in the Java heap. - * Instances can only be used to perform a single allocation, which helps - * ensure that the allocated buffer is properly accounted for with a - * reference in the heap (or a JNI global reference). - */ -class JavaPixelAllocator : public SkBRDAllocator { +class HeapAllocator : public SkBRDAllocator { public: - explicit JavaPixelAllocator(JNIEnv* env); - ~JavaPixelAllocator(); + HeapAllocator(); + ~HeapAllocator(); virtual bool allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) override; @@ -140,14 +134,8 @@ public: return result; }; - /** - * Indicates that this allocator allocates zero initialized - * memory. - */ SkCodec::ZeroInitialized zeroInit() const override { return SkCodec::kYes_ZeroInitialized; } - private: - JavaVM* mJavaVM; android::Bitmap* mStorage = nullptr; }; diff --git a/core/jni/android_os_Debug.cpp b/core/jni/android_os_Debug.cpp index e0bfecb14040..97c7f049fdb9 100644 --- a/core/jni/android_os_Debug.cpp +++ b/core/jni/android_os_Debug.cpp @@ -839,7 +839,8 @@ extern "C" void get_malloc_leak_info(uint8_t** info, size_t* overallSize, size_t* infoSize, size_t* totalMemory, size_t* backtraceSize); extern "C" void free_malloc_leak_info(uint8_t* info); #define SIZE_FLAG_ZYGOTE_CHILD (1<<31) -#define BACKTRACE_SIZE 32 + +static size_t gNumBacktraceElements; /* * This is a qsort() callback. @@ -859,11 +860,11 @@ static int compareHeapRecords(const void* vrec1, const void* vrec2) return -1; } - intptr_t* bt1 = (intptr_t*)(rec1 + 2); - intptr_t* bt2 = (intptr_t*)(rec2 + 2); - for (size_t idx = 0; idx < BACKTRACE_SIZE; idx++) { - intptr_t addr1 = bt1[idx]; - intptr_t addr2 = bt2[idx]; + uintptr_t* bt1 = (uintptr_t*)(rec1 + 2); + uintptr_t* bt2 = (uintptr_t*)(rec2 + 2); + for (size_t idx = 0; idx < gNumBacktraceElements; idx++) { + uintptr_t addr1 = bt1[idx]; + uintptr_t addr2 = bt2[idx]; if (addr1 == addr2) { if (addr1 == 0) break; @@ -907,9 +908,10 @@ static void dumpNativeHeap(FILE* fp) if (info == NULL) { fprintf(fp, "Native heap dump not available. To enable, run these" " commands (requires root):\n"); - fprintf(fp, "$ adb shell setprop libc.debug.malloc 1\n"); - fprintf(fp, "$ adb shell stop\n"); - fprintf(fp, "$ adb shell start\n"); + fprintf(fp, "# adb shell stop\n"); + fprintf(fp, "# adb shell setprop libc.debug.malloc.options " + "backtrace\n"); + fprintf(fp, "# adb shell start\n"); return; } assert(infoSize != 0); @@ -920,13 +922,11 @@ static void dumpNativeHeap(FILE* fp) size_t recordCount = overallSize / infoSize; fprintf(fp, "Total memory: %zu\n", totalMemory); fprintf(fp, "Allocation records: %zd\n", recordCount); - if (backtraceSize != BACKTRACE_SIZE) { - fprintf(fp, "WARNING: mismatched backtrace sizes (%zu vs. %d)\n", - backtraceSize, BACKTRACE_SIZE); - } + fprintf(fp, "Backtrace size: %zd\n", backtraceSize); fprintf(fp, "\n"); /* re-sort the entries */ + gNumBacktraceElements = backtraceSize; qsort(info, recordCount, infoSize, compareHeapRecords); /* dump the entries to the file */ @@ -934,7 +934,7 @@ static void dumpNativeHeap(FILE* fp) for (size_t idx = 0; idx < recordCount; idx++) { size_t size = *(size_t*) ptr; size_t allocations = *(size_t*) (ptr + sizeof(size_t)); - intptr_t* backtrace = (intptr_t*) (ptr + sizeof(size_t) * 2); + uintptr_t* backtrace = (uintptr_t*) (ptr + sizeof(size_t) * 2); fprintf(fp, "z %d sz %8zu num %4zu bt", (size & SIZE_FLAG_ZYGOTE_CHILD) != 0, diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 357d6f962229..a08c63e2f1cf 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -290,6 +290,7 @@ <protected-broadcast android:name="android.net.wifi.WIFI_AP_STATE_CHANGED" /> <protected-broadcast android:name="android.net.wifi.WIFI_CREDENTIAL_CHANGED" /> <protected-broadcast android:name="android.net.wifi.WIFI_SCAN_AVAILABLE" /> + <protected-broadcast android:name="android.net.wifi.nan.action.WIFI_NAN_STATE_CHANGED" /> <protected-broadcast android:name="android.net.wifi.SCAN_RESULTS" /> <protected-broadcast android:name="android.net.wifi.RSSI_CHANGED" /> <protected-broadcast android:name="android.net.wifi.STATE_CHANGE" /> diff --git a/core/res/res/values-af/strings.xml b/core/res/res/values-af/strings.xml index f58852009ecd..34244ab1863d 100644 --- a/core/res/res/values-af/strings.xml +++ b/core/res/res/values-af/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g>-USB-datastokkie"</string> <string name="storage_usb" msgid="3017954059538517278">"USB-berging"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Redigeer"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Dataverbruik-waarskuwing"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Tik om gebruik en instellings te bekyk."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G-datalimiet bereik"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G-datalimiet bereik"</string> diff --git a/core/res/res/values-am/strings.xml b/core/res/res/values-am/strings.xml index ac2e76b03078..b3cd896307bb 100644 --- a/core/res/res/values-am/strings.xml +++ b/core/res/res/values-am/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"የ<xliff:g id="MANUFACTURER">%s</xliff:g> ዩኤስቢ አንጻፊ"</string> <string name="storage_usb" msgid="3017954059538517278">"የUSB ማከማቻ"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"አርትዕ"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"የውሂብ አጠቃቀም ማስጠንቀቂየ"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"አጠቃቀምን እና ቅንብሮችን ለማየት መታ ያድርጉ።"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"የ2ጂ-3ጂ ውሂብ ገደብ ላይ ተደርሷል"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"የ4ጂ ውሂብ ገደብ ላይ ተደርሷል"</string> diff --git a/core/res/res/values-ar/strings.xml b/core/res/res/values-ar/strings.xml index 380561266dbc..717e9da989dd 100644 --- a/core/res/res/values-ar/strings.xml +++ b/core/res/res/values-ar/strings.xml @@ -1434,7 +1434,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"محرك أقراص USB من <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"وحدة تخزين USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"تعديل"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"تحذير استخدام البيانات"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"انقر لعرض الاستخدام والإعدادات."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"تم بلوغ حد بيانات اتصال 2G-3G"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"تم بلوغ حد بيانات اتصال 4G"</string> diff --git a/core/res/res/values-az-rAZ/strings.xml b/core/res/res/values-az-rAZ/strings.xml index d3ee4a4cb6c8..5ff1373331ee 100644 --- a/core/res/res/values-az-rAZ/strings.xml +++ b/core/res/res/values-az-rAZ/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB drayv"</string> <string name="storage_usb" msgid="3017954059538517278">"USB yaddaş"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Düzəliş edin"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Data istifadə xəbərdarlığı"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"İstifadə və ayarları görmək üçün tıklayın."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G data limitinə çatdı"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G data limitinə çatdı"</string> diff --git a/core/res/res/values-b+sr+Latn/strings.xml b/core/res/res/values-b+sr+Latn/strings.xml index 72671f136004..a351d5cb01fc 100644 --- a/core/res/res/values-b+sr+Latn/strings.xml +++ b/core/res/res/values-b+sr+Latn/strings.xml @@ -1356,7 +1356,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB disk"</string> <string name="storage_usb" msgid="3017954059538517278">"USB memorija"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Izmeni"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Upozorenje o potrošnji podataka"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Dodirnite za potrošnju i podešavanja."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Nema više 2G-3G podataka"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Nema više 4G podataka"</string> diff --git a/core/res/res/values-be-rBY/strings.xml b/core/res/res/values-be-rBY/strings.xml index 3f2e07c41090..13a9cdcef2fd 100644 --- a/core/res/res/values-be-rBY/strings.xml +++ b/core/res/res/values-be-rBY/strings.xml @@ -1382,7 +1382,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"USB-дыск <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"USB-назапашвальнік"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Рэдагаваць"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Папярэджанне выкарыстання дадзеных"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Прагляд выкарыстання і налад."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Дасягнуты ліміт трафіку 2G-3G"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Дасягнуты ліміт трафіку 4G"</string> diff --git a/core/res/res/values-bg/strings.xml b/core/res/res/values-bg/strings.xml index e7ac345e0100..95e89aff9b64 100644 --- a/core/res/res/values-bg/strings.xml +++ b/core/res/res/values-bg/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"USB устройство от <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"USB хранилище"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Редактиране"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Предупрежд. за ползване на данни"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Пренос и настройки: Докоснете за преглед."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Достигнат лимит за 2G/3G данните"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Достигнат лимит за 4G данните"</string> diff --git a/core/res/res/values-bn-rBD/strings.xml b/core/res/res/values-bn-rBD/strings.xml index b69a123160e6..25f3367dfbe3 100644 --- a/core/res/res/values-bn-rBD/strings.xml +++ b/core/res/res/values-bn-rBD/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB ড্রাইভ"</string> <string name="storage_usb" msgid="3017954059538517278">"USB সঞ্চয়স্থান"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"সম্পাদনা করুন"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"ডেটা ব্যবহারের সতর্কতা"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"ব্যবহার এবং সেটিংস দেখতে আলতো চাপুন৷"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G ডেটা সীমা ছাড়িয়েছে"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G ডেটা সীমা ছাড়িয়েছে"</string> diff --git a/core/res/res/values-bs-rBA/strings.xml b/core/res/res/values-bs-rBA/strings.xml index 019ff1d60709..e786ff9b3aee 100644 --- a/core/res/res/values-bs-rBA/strings.xml +++ b/core/res/res/values-bs-rBA/strings.xml @@ -1358,7 +1358,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB disk"</string> <string name="storage_usb" msgid="3017954059538517278">"USB pohrana"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Uredi"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Upozorenje za prijenos podataka"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Dodirnite za prikaz upotrebe i postavki."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Dostignut limit za 2G-3G podatke"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Dostignut limit za 4G podatke"</string> diff --git a/core/res/res/values-ca/strings.xml b/core/res/res/values-ca/strings.xml index 8926fce19563..ac0b6d39b508 100644 --- a/core/res/res/values-ca/strings.xml +++ b/core/res/res/values-ca/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Unitat USB de: <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Emmagatzematge USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Edita"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Advertiment d\'ús de dades"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Toca per veure l\'ús i la configuració."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Límit de dades 2G-3G assolit"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Límit de dades 4G assolit"</string> diff --git a/core/res/res/values-cs/strings.xml b/core/res/res/values-cs/strings.xml index a9ff27d17c3c..4370d9f635d9 100644 --- a/core/res/res/values-cs/strings.xml +++ b/core/res/res/values-cs/strings.xml @@ -1382,7 +1382,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Jednotka USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Úložiště USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Upravit"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Upozornění na využití dat"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Klepnutím zobrazíte nastavení."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Dosáhli jste limitu dat 2G–3G"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Dosáhli jste limitu dat 4G"</string> diff --git a/core/res/res/values-da/strings.xml b/core/res/res/values-da/strings.xml index 487432b13132..1688cbe9a343 100644 --- a/core/res/res/values-da/strings.xml +++ b/core/res/res/values-da/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"USB-drev fra <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"USB-lager"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Rediger"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Advarsel om dataforbrug"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Tryk for at se forbrug og indstillinger."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Grænsen for 2G-3G-data er nået"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Grænsen for 4G-data er nået"</string> diff --git a/core/res/res/values-de/strings.xml b/core/res/res/values-de/strings.xml index 6b0525322410..d13777bd0577 100644 --- a/core/res/res/values-de/strings.xml +++ b/core/res/res/values-de/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"USB-Speichergerät von <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"USB-Speicher"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Bearbeiten"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Warnung zum Datenverbrauch"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Für Nutzung und Einstellungen tippen."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-/3G-Datenlimit erreicht"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G-Datenlimit erreicht"</string> diff --git a/core/res/res/values-el/strings.xml b/core/res/res/values-el/strings.xml index a6ee8c2b8f3e..8f63a7355029 100644 --- a/core/res/res/values-el/strings.xml +++ b/core/res/res/values-el/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Μονάδα USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Αποθηκευτικός χώρος USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Επεξεργασία"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Προειδοποίηση χρήσης δεδομένων"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Πατήστε για προβολή χρήσης/ρυθμ."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Συμπλ. το όριο δεδομένων 2G-3G"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Συμπλ. το όριο δεδομένων 4G"</string> diff --git a/core/res/res/values-en-rAU/strings.xml b/core/res/res/values-en-rAU/strings.xml index 6342966f1ffc..348a7de5629e 100644 --- a/core/res/res/values-en-rAU/strings.xml +++ b/core/res/res/values-en-rAU/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB drive"</string> <string name="storage_usb" msgid="3017954059538517278">"USB storage"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Edit"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Data usage warning"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Tap to view usage and settings."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G data limit reached"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G data limit reached"</string> diff --git a/core/res/res/values-en-rGB/strings.xml b/core/res/res/values-en-rGB/strings.xml index 6342966f1ffc..348a7de5629e 100644 --- a/core/res/res/values-en-rGB/strings.xml +++ b/core/res/res/values-en-rGB/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB drive"</string> <string name="storage_usb" msgid="3017954059538517278">"USB storage"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Edit"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Data usage warning"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Tap to view usage and settings."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G data limit reached"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G data limit reached"</string> diff --git a/core/res/res/values-en-rIN/strings.xml b/core/res/res/values-en-rIN/strings.xml index 6342966f1ffc..348a7de5629e 100644 --- a/core/res/res/values-en-rIN/strings.xml +++ b/core/res/res/values-en-rIN/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB drive"</string> <string name="storage_usb" msgid="3017954059538517278">"USB storage"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Edit"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Data usage warning"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Tap to view usage and settings."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G data limit reached"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G data limit reached"</string> diff --git a/core/res/res/values-es-rUS/strings.xml b/core/res/res/values-es-rUS/strings.xml index 0ba371fe0872..39746e0de81f 100644 --- a/core/res/res/values-es-rUS/strings.xml +++ b/core/res/res/values-es-rUS/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Unidad USB de <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Almacenamiento USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Editar"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Advertencia de uso de datos"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Presiona para uso y opciones."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Límite de datos 2G-3G alcanzado"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Límite de datos 4G alcanzado"</string> diff --git a/core/res/res/values-es/strings.xml b/core/res/res/values-es/strings.xml index d60fa8342929..117205f6c4e7 100644 --- a/core/res/res/values-es/strings.xml +++ b/core/res/res/values-es/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Unidad USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Almacenamiento USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Editar"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Advertencia de uso de datos"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Toca para ver uso y ajustes."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Límite de datos 2G-3G alcanzado"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Límite de datos 4G alcanzado"</string> diff --git a/core/res/res/values-et-rEE/strings.xml b/core/res/res/values-et-rEE/strings.xml index 15fa79fca1da..2fadecbd075c 100644 --- a/core/res/res/values-et-rEE/strings.xml +++ b/core/res/res/values-et-rEE/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Tootja <xliff:g id="MANUFACTURER">%s</xliff:g> USB-ketas"</string> <string name="storage_usb" msgid="3017954059538517278">"USB-mäluseade"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Muuda"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Andmete kasutamise hoiatus"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Puudutage kasutuse/seadete vaat."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-, 3G-andmeside limiit on täis"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G-andmeside limiit on täis"</string> diff --git a/core/res/res/values-eu-rES/strings.xml b/core/res/res/values-eu-rES/strings.xml index 47e64c3dda8d..61799870313f 100644 --- a/core/res/res/values-eu-rES/strings.xml +++ b/core/res/res/values-eu-rES/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB unitatea"</string> <string name="storage_usb" msgid="3017954059538517278">"USB memoria"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Editatu"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Datuen erabilerari buruzko abisua"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Sakatu erabilera eta ezarpenak ikusteko."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2-3 GB-ko mugara iritsi zara"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4 GB-ko mugara iritsi zara"</string> diff --git a/core/res/res/values-fa/strings.xml b/core/res/res/values-fa/strings.xml index 95c0db716b91..fd2bf8125859 100644 --- a/core/res/res/values-fa/strings.xml +++ b/core/res/res/values-fa/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"درایو USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"حافظهٔ USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"ویرایش"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"هشدار میزان استفاده از داده"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"برای مشاهده مصرف و تنظیمات ضربه بزنید."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"به حد مجاز مصرف داده 2G-3G رسید"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"به حد مجاز مصرف داده 4G رسید"</string> diff --git a/core/res/res/values-fi/strings.xml b/core/res/res/values-fi/strings.xml index e11b2ca719a7..3ea7e66bfc61 100644 --- a/core/res/res/values-fi/strings.xml +++ b/core/res/res/values-fi/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"USB-asema: <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"USB-tallennustila"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Muokkaa"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Tiedonsiirtovaroitus"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Käyttö & asetukset napauttamalla"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G-tietojen raja saavutettu"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G-tietojen raja saavutettu"</string> diff --git a/core/res/res/values-fr-rCA/strings.xml b/core/res/res/values-fr-rCA/strings.xml index c1e1808ed304..8b74244b4088 100644 --- a/core/res/res/values-fr-rCA/strings.xml +++ b/core/res/res/values-fr-rCA/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Clé USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Mémoire de stockage USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Modifier"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Avertissement utilisation données"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Touch. pour aff. util. et param."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Limite de données 2G-3G atteinte"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Limite de données 4G atteinte"</string> diff --git a/core/res/res/values-fr/strings.xml b/core/res/res/values-fr/strings.xml index 850301f9794d..8c8c6833c407 100644 --- a/core/res/res/values-fr/strings.xml +++ b/core/res/res/values-fr/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Clé USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Mémoire de stockage USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Modifier"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Avertissement utilisation données"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Appuyez pour conso/paramètres."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Limite de données 2G-3G atteinte"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Limite de données 4G atteinte"</string> diff --git a/core/res/res/values-gl-rES/strings.xml b/core/res/res/values-gl-rES/strings.xml index 8a37cffdd908..f842df2c85a3 100644 --- a/core/res/res/values-gl-rES/strings.xml +++ b/core/res/res/values-gl-rES/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Unidade USB de <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"almacenamento USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Editar"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Aviso de uso de datos"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Toca para uso e configuración."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Límite de datos de 2G-3G acadado"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Límite de datos de 4G acadado"</string> diff --git a/core/res/res/values-gu-rIN/strings.xml b/core/res/res/values-gu-rIN/strings.xml index 2cd3da8ed45c..45038b0a6cf6 100644 --- a/core/res/res/values-gu-rIN/strings.xml +++ b/core/res/res/values-gu-rIN/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB ડ્રાઇવ"</string> <string name="storage_usb" msgid="3017954059538517278">"USB સંગ્રહ"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"સંપાદિત કરો"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"ડેટા વપરાશ ચેતવણી"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"વપરાશ અને સેટિંગ્સ જોવા ટૅપ કરો."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G ડેટા મર્યાદા પર પહોંચ્યાં"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G ડેટા મર્યાદા સુધી પહોંચ્યાં"</string> diff --git a/core/res/res/values-hi/strings.xml b/core/res/res/values-hi/strings.xml index d87f747fe631..8a651bda5a6d 100644 --- a/core/res/res/values-hi/strings.xml +++ b/core/res/res/values-hi/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB डिस्क"</string> <string name="storage_usb" msgid="3017954059538517278">"USB मेमोरी"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"संपादित करें"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"डेटा उपयोग की चेतावनी"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"उपयोग व सेटिंग देखने हेतु टैप करें."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G डेटा सीमा पूर्ण हो गई"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G डेटा सीमा पूर्ण हो गई"</string> diff --git a/core/res/res/values-hr/strings.xml b/core/res/res/values-hr/strings.xml index ed55b3f79992..16a304fb3006 100644 --- a/core/res/res/values-hr/strings.xml +++ b/core/res/res/values-hr/strings.xml @@ -1356,7 +1356,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB pogon"</string> <string name="storage_usb" msgid="3017954059538517278">"USB pohrana"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Uredi"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Upozorenje o upotrebi podataka"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Dodirnite za upotrebu i postavke"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Dost. ogr. 2G–3G prijenosa pod."</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Dost. ogr. 4G prijenosa podataka"</string> diff --git a/core/res/res/values-hu/strings.xml b/core/res/res/values-hu/strings.xml index f4ea98d67095..cbb72a7c06c1 100644 --- a/core/res/res/values-hu/strings.xml +++ b/core/res/res/values-hu/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB-meghajtó"</string> <string name="storage_usb" msgid="3017954059538517278">"USB-tár"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Szerkesztés"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Adathasználati figyelmeztetés"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Koppintson az adatokért."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-/3G-adatkorlát elérve"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G-adatkorlát elérve"</string> diff --git a/core/res/res/values-hy-rAM/strings.xml b/core/res/res/values-hy-rAM/strings.xml index f43dfb3009f7..ae83552af2c9 100644 --- a/core/res/res/values-hy-rAM/strings.xml +++ b/core/res/res/values-hy-rAM/strings.xml @@ -251,17 +251,17 @@ <string name="permgrouplab_calendar" msgid="5863508437783683902">"Օրացույց"</string> <string name="permgroupdesc_calendar" msgid="3889615280211184106">"օգտագործել օրացույցը"</string> <string name="permgrouplab_sms" msgid="228308803364967808">"Կարճ հաղորդագրություն"</string> - <string name="permgroupdesc_sms" msgid="4656988620100940350">"ուղարկել և դիտել SMS հաղորդագրությունները"</string> + <string name="permgroupdesc_sms" msgid="4656988620100940350">"ուղարկել և դիտել SMS հաղորդ․-ները"</string> <string name="permgrouplab_storage" msgid="1971118770546336966">"Պահոց"</string> - <string name="permgroupdesc_storage" msgid="637758554581589203">"օգտագործել լուսանկարները, մեդիա ֆայլերը և ձեր սարքում պահվող այլ ֆայլերը"</string> + <string name="permgroupdesc_storage" msgid="637758554581589203">"օգտագործել լուսանկարները, մեդիա ֆայլերը և ձեր սարքում պահվող մյուս ֆայլերը"</string> <string name="permgrouplab_microphone" msgid="171539900250043464">"Բարձրախոս"</string> - <string name="permgroupdesc_microphone" msgid="4988812113943554584">"ձայնագրում"</string> + <string name="permgroupdesc_microphone" msgid="4988812113943554584">"ձայնագրել"</string> <string name="permgrouplab_camera" msgid="4820372495894586615">"Ֆոտոխցիկ"</string> - <string name="permgroupdesc_camera" msgid="3250611594678347720">"լուսանկարում և տեսագրում"</string> + <string name="permgroupdesc_camera" msgid="3250611594678347720">"լուսանկարել և տեսագրել"</string> <string name="permgrouplab_phone" msgid="5229115638567440675">"Հեռախոս"</string> - <string name="permgroupdesc_phone" msgid="6234224354060641055">"հեռախոսազանգերի կատարում և կառավարում"</string> + <string name="permgroupdesc_phone" msgid="6234224354060641055">"կատարել զանգեր և կառավարել զանգերը"</string> <string name="permgrouplab_sensors" msgid="416037179223226722">"Մարմնի սենսորներ"</string> - <string name="permgroupdesc_sensors" msgid="7147968539346634043">"օգտագործել ձեր հիմնական ֆիզիոլոգիական ցուցանիշների վերաբերյալ սենսորի տվյալները"</string> + <string name="permgroupdesc_sensors" msgid="7147968539346634043">"օգտագործել սենսորների տվյալները ձեր օրգանիզմի վիճակի մասին"</string> <string name="capability_title_canRetrieveWindowContent" msgid="3901717936930170320">"Առբերել պատուհանի բովանդակությունը"</string> <string name="capability_desc_canRetrieveWindowContent" msgid="3772225008605310672">"Ստուգեք պատուհանի բովանդակությունը, որի հետ փոխգործակցում եք:"</string> <string name="capability_title_canRequestTouchExploration" msgid="3108723364676667320">"Միացնել Հպման միջոցով հետազոտումը"</string> @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"USB սարքավար <xliff:g id="MANUFACTURER">%s</xliff:g>-ից"</string> <string name="storage_usb" msgid="3017954059538517278">"USB կրիչ"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Խմբագրել"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Տվյալների օգտագործման նախազգուշացում"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Հպեք և տեսեք օգտագործումը և կարգավորումները:"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G տվյալների սահմանաչափը սպառվել է"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G տվյալների սահմանաչափը սպառվել է"</string> diff --git a/core/res/res/values-in/strings.xml b/core/res/res/values-in/strings.xml index 2cbb6fb5b8a1..b6f6c4de6483 100644 --- a/core/res/res/values-in/strings.xml +++ b/core/res/res/values-in/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Drive USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Penyimpanan USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Edit"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Peringatan penggunaan data"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Ketuk untuk lihat penggunaan & setelan."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Batas data 2G-3G terlampaui"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Batas data 4G terlampaui"</string> diff --git a/core/res/res/values-is-rIS/strings.xml b/core/res/res/values-is-rIS/strings.xml index ffd1e311fa5a..5cce18b6f705 100644 --- a/core/res/res/values-is-rIS/strings.xml +++ b/core/res/res/values-is-rIS/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"USB-drif frá <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"USB-geymsla"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Breyta"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Viðvörun vegna gagnanotkunar"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Ýttu fyrir uppl. og stillingar"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Gagnahámarki 2G og 3G náð"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Gagnahámarki 4G náð"</string> diff --git a/core/res/res/values-it/strings.xml b/core/res/res/values-it/strings.xml index e576d8daf4d9..13c17ce073f5 100644 --- a/core/res/res/values-it/strings.xml +++ b/core/res/res/values-it/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Unità USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Archivio USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Modifica"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Avviso sull\'utilizzo dei dati"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Tocca per uso e impostazioni."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Limite di dati 2G-3G raggiunto"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Limite di dati 4G raggiunto"</string> diff --git a/core/res/res/values-iw/strings.xml b/core/res/res/values-iw/strings.xml index c0c18718e257..87ec61a4ec50 100644 --- a/core/res/res/values-iw/strings.xml +++ b/core/res/res/values-iw/strings.xml @@ -1382,7 +1382,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"כונן USB של <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"אחסון USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"ערוך"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"אזהרת שימוש בנתונים"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"הקש כדי להציג נתוני שימוש והגדרות."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"הגעת למגבלת הנתונים של 2G-3G"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"הגעת למגבלת הנתונים של 4G"</string> diff --git a/core/res/res/values-ja/strings.xml b/core/res/res/values-ja/strings.xml index 59247378445f..213c0641aabb 100644 --- a/core/res/res/values-ja/strings.xml +++ b/core/res/res/values-ja/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g>製USBドライブ"</string> <string name="storage_usb" msgid="3017954059538517278">"USBストレージ"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"編集"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"データ使用の警告"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"タップして使用状況と設定を表示します。"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G~3Gデータの上限に達しました"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4Gデータの上限に達しました"</string> diff --git a/core/res/res/values-ka-rGE/strings.xml b/core/res/res/values-ka-rGE/strings.xml index b5635914fd45..315ff645dda8 100644 --- a/core/res/res/values-ka-rGE/strings.xml +++ b/core/res/res/values-ka-rGE/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB დისკი"</string> <string name="storage_usb" msgid="3017954059538517278">"USB მეხსიერება"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"რედაქტირება"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"ინტერნეტის გამოყენების გაფრთხილება"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"შეეხეთ მოხმარებისა და პარამეტრების სანახავად."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G მონაცემთა ლიმიტი ამოიწურა"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G მონაცემთა ლიმიტი ამოიწურა"</string> diff --git a/core/res/res/values-kk-rKZ/strings.xml b/core/res/res/values-kk-rKZ/strings.xml index e752abace139..525fbeeeea96 100644 --- a/core/res/res/values-kk-rKZ/strings.xml +++ b/core/res/res/values-kk-rKZ/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB дискі"</string> <string name="storage_usb" msgid="3017954059538517278">"USB жады"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Өзгерту"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Дерекқор қолдануға қатысты ескерту"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Трафик пен параметрлерді көру үшін түртіңіз."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G деректер шегіне жеттіңіз"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G деректер шегіне жеттіңіз"</string> diff --git a/core/res/res/values-km-rKH/strings.xml b/core/res/res/values-km-rKH/strings.xml index 3fcf41c4e01b..014d8446991d 100644 --- a/core/res/res/values-km-rKH/strings.xml +++ b/core/res/res/values-km-rKH/strings.xml @@ -1332,7 +1332,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"ឧបករណ៍ផ្ទុក USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"ឧបករណ៍ផ្ទុកយូអេសប៊ី"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"កែសម្រួល"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"ការព្រមានប្រើទិន្នន័យ"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"ប៉ះដើម្បីមើលការប្រើប្រាស់ និងការកំណត់"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"បានដល់ដែនកំណត់ទិន្នន័យ 2G-3G"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"បានដល់ដែនកំណត់ទិន្នន័យ 4G"</string> diff --git a/core/res/res/values-kn-rIN/strings.xml b/core/res/res/values-kn-rIN/strings.xml index cd3f94816399..ab60289fb29c 100644 --- a/core/res/res/values-kn-rIN/strings.xml +++ b/core/res/res/values-kn-rIN/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB ಡ್ರೈವ್"</string> <string name="storage_usb" msgid="3017954059538517278">"USB ಸಂಗ್ರಹಣೆ"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"ಎಡಿಟ್"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"ಡೇಟಾ ಬಳಕೆಯ ಎಚ್ಚರಿಕೆ"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"ಬಳಕೆ ಮತ್ತು ಸೆಟ್ಟಿಂಗ್ಗಳನ್ನು ವೀಕ್ಷಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G ಡೇಟಾ ಮೀತಿಯನ್ನು ತಲುಪಿದೆ"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G ಡೇಟಾ ಮೀತಿಯನ್ನು ತಲುಪಿದೆ"</string> diff --git a/core/res/res/values-ko/strings.xml b/core/res/res/values-ko/strings.xml index 8de15a8b3520..5d226a6bd822 100644 --- a/core/res/res/values-ko/strings.xml +++ b/core/res/res/values-ko/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB 드라이브"</string> <string name="storage_usb" msgid="3017954059538517278">"USB 저장소"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"수정"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"데이터 사용 경고"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"사용량 및 설정을 보려면 탭하세요."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G 데이터 한도에 도달함"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G 데이터 한도에 도달함"</string> diff --git a/core/res/res/values-ky-rKG/strings.xml b/core/res/res/values-ky-rKG/strings.xml index 2d155bf82218..99bba3a72e7c 100644 --- a/core/res/res/values-ky-rKG/strings.xml +++ b/core/res/res/values-ky-rKG/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB түзмөгү"</string> <string name="storage_usb" msgid="3017954059538517278">"USB эстутуму"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Өзгөртүү"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Дайындарды колдонуу боюнча эскрт"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Колдонулушун жана жөндөөлөрүн көрүү үчүн таптаңыз."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G дайындар чегине жетти"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G дайындар чегине жетти"</string> diff --git a/core/res/res/values-lo-rLA/strings.xml b/core/res/res/values-lo-rLA/strings.xml index 0676d1508921..9762f264afed 100644 --- a/core/res/res/values-lo-rLA/strings.xml +++ b/core/res/res/values-lo-rLA/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB ດຣ້າຍ"</string> <string name="storage_usb" msgid="3017954059538517278">"ບ່ອນຈັດເກັບຂໍ້ມູນ USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"ແກ້ໄຂ"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"ເຕືອນກ່ຽວກັບການນຳໃຊ້ຂໍ້ມູນ"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"ແຕະເພື່ອເບິ່ງການນຳໃຊ້ ແລະ ການຕັ້ງຄ່າ."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"ໃຊ້ຂໍ້ມູນ 2G-3G ຮອດຈຳນວນທີ່ຈຳກັດແລ້ວ"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"ໃຊ້ຂໍ້ມູນ 4G ຮອດຈຳນວນທີ່ຈຳກັດແລ້ວ"</string> diff --git a/core/res/res/values-lt/strings.xml b/core/res/res/values-lt/strings.xml index f43e6e814029..644606661b22 100644 --- a/core/res/res/values-lt/strings.xml +++ b/core/res/res/values-lt/strings.xml @@ -1382,7 +1382,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"„<xliff:g id="MANUFACTURER">%s</xliff:g>“ atmintukas"</string> <string name="storage_usb" msgid="3017954059538517278">"USB atmintis"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Redaguoti"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Įspėjimas dėl duomenų naudojimo"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Pal. ir perž. naud. i. bei nust."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Pasiektas 2G–3G duomenų apribojimas"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Pasiektas 4G duomenų apribojimas"</string> diff --git a/core/res/res/values-lv/strings.xml b/core/res/res/values-lv/strings.xml index 3aa408a391dc..b0e7af501b17 100644 --- a/core/res/res/values-lv/strings.xml +++ b/core/res/res/values-lv/strings.xml @@ -1356,7 +1356,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB disks"</string> <string name="storage_usb" msgid="3017954059538517278">"USB atmiņa"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Rediģēt"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Datu izmantošanas brīdinājums"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Piesk., lai sk. lietoj. un iest."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Sasniegts 2G-3G datu ierobež."</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Sasniegts 4G datu ierobežojums"</string> diff --git a/core/res/res/values-mcc238-mnc06/config.xml b/core/res/res/values-mcc238-mnc06/config.xml deleted file mode 100644 index afc0cc4011a0..000000000000 --- a/core/res/res/values-mcc238-mnc06/config.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* -** Copyright 2014, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ ---> - -<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <!-- SIM does not save, but the voice mail number to be changed. --> - <bool name="editable_voicemailnumber">true</bool> -</resources> diff --git a/core/res/res/values-mk-rMK/strings.xml b/core/res/res/values-mk-rMK/strings.xml index 437df10805c0..a47a48341627 100644 --- a/core/res/res/values-mk-rMK/strings.xml +++ b/core/res/res/values-mk-rMK/strings.xml @@ -1332,7 +1332,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> УСБ-меморија"</string> <string name="storage_usb" msgid="3017954059538517278">"УСБ меморија"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Уреди"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Опомена за потрошен интернет"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Допрете за употреба и поставки."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Постигна лимит за 2G-3G податоци"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Постигнат лимит за 4G податоци"</string> diff --git a/core/res/res/values-ml-rIN/strings.xml b/core/res/res/values-ml-rIN/strings.xml index 76d05c783b79..eaa8deb68d74 100644 --- a/core/res/res/values-ml-rIN/strings.xml +++ b/core/res/res/values-ml-rIN/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB ഡ്രൈവ്"</string> <string name="storage_usb" msgid="3017954059538517278">"USB സ്റ്റോറേജ്"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"എഡിറ്റുചെയ്യുക"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"ഡാറ്റ ഉപയോഗ മുന്നറിയിപ്പ്"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"ഉപയോഗവും ക്രമീകരണവും കാണാൻ ടാപ്പുചെയ്യുക."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G ഡാറ്റ പരിധിയിലെത്തി"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G ഡാറ്റ പരിധിയിലെത്തി"</string> diff --git a/core/res/res/values-mn-rMN/strings.xml b/core/res/res/values-mn-rMN/strings.xml index 074e5c3c643b..79dbc686d967 100644 --- a/core/res/res/values-mn-rMN/strings.xml +++ b/core/res/res/values-mn-rMN/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB диск"</string> <string name="storage_usb" msgid="3017954059538517278">"USB сан"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Засах"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Дата хэрэглээний анхааруулга"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Хэрэглээ, тохиргоог харах бол товш."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G дата хязгаарт хүрсэн"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G дата хязгаарт хүрсэн"</string> diff --git a/core/res/res/values-mr-rIN/strings.xml b/core/res/res/values-mr-rIN/strings.xml index 1997714eefd5..fce64c424353 100644 --- a/core/res/res/values-mr-rIN/strings.xml +++ b/core/res/res/values-mr-rIN/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB ड्राइव्ह"</string> <string name="storage_usb" msgid="3017954059538517278">"USB संचयन"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"संपादित करा"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"डेटा वापर चेतावणी"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"वापर आणि सेटिंग्ज पाहण्यासाठी टॅप करा."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G डेटा मर्यादा गाठली"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G डेटा मर्यादा गाठली"</string> diff --git a/core/res/res/values-ms-rMY/strings.xml b/core/res/res/values-ms-rMY/strings.xml index 0846ede2b3d0..53181ce0fe49 100644 --- a/core/res/res/values-ms-rMY/strings.xml +++ b/core/res/res/values-ms-rMY/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Pemacu USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Storan USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Edit"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Amaran penggunaan data"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Ketik utk lihat p\'gunaan & ttpn."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Mencapai had data 2G-3G"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Mencapai had data 4G"</string> diff --git a/core/res/res/values-my-rMM/strings.xml b/core/res/res/values-my-rMM/strings.xml index 549bb69ede27..987d7642fdf3 100644 --- a/core/res/res/values-my-rMM/strings.xml +++ b/core/res/res/values-my-rMM/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB ဒရိုက်ဗ်"</string> <string name="storage_usb" msgid="3017954059538517278">"USBဖြင့် သိမ်းဆည်း"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"ပြင်ဆင်ရန်"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"ဒေတာအသုံးပြုမှုသတိပေးချက်"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"အသုံးပြုမှုနှင့် ဆက်တင်များကိုကြည့်ရန် တို့ပါ။"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G ဒေတာ ကန့်သတ်ချက် ပြည့်မီသွားပြီ"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G ဒေတာ ကန့်သတ်ချက် ပြည့်မီသွားပြီ"</string> diff --git a/core/res/res/values-nb/strings.xml b/core/res/res/values-nb/strings.xml index 33f1436c7158..c807c14877bd 100644 --- a/core/res/res/values-nb/strings.xml +++ b/core/res/res/values-nb/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB-stasjon"</string> <string name="storage_usb" msgid="3017954059538517278">"USB-lagring"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Rediger"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Advarsel for høyt dataforbruk"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Trykk for å se bruken og innstillingene."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Datagrensen for 2G-3G er nådd"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Datagrensen for 4G er nådd"</string> diff --git a/core/res/res/values-ne-rNP/strings.xml b/core/res/res/values-ne-rNP/strings.xml index d4ad51f55599..65d3b69e923c 100644 --- a/core/res/res/values-ne-rNP/strings.xml +++ b/core/res/res/values-ne-rNP/strings.xml @@ -257,7 +257,7 @@ <string name="permgrouplab_microphone" msgid="171539900250043464">"माइक्रोफोन"</string> <string name="permgroupdesc_microphone" msgid="4988812113943554584">"अडियो रेकर्ड गर्नुहोस्"</string> <string name="permgrouplab_camera" msgid="4820372495894586615">"क्यामेरा"</string> - <string name="permgroupdesc_camera" msgid="3250611594678347720">"तस्बिर खिच्नुहोस् तथा भिडियो रेकर्ड गर्नुहोस्"</string> + <string name="permgroupdesc_camera" msgid="3250611594678347720">"तस्बिर खिच्नुका साथै भिडियो रेकर्ड गर्नुहोस्"</string> <string name="permgrouplab_phone" msgid="5229115638567440675">"फोन"</string> <string name="permgroupdesc_phone" msgid="6234224354060641055">"फोन कलहरू गर्नुहोस् र व्यवस्थापन गर्नुहोस्"</string> <string name="permgrouplab_sensors" msgid="416037179223226722">"शारीरिक सेन्सर"</string> @@ -1336,7 +1336,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB ड्राइभ"</string> <string name="storage_usb" msgid="3017954059538517278">"USB भण्डारण"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"सम्पादन गर्नुहोस्"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"डेटाको प्रयोग चेतावनी"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"प्रयोग र सेटिङहरू हेर्न ट्याप गर्नुहोस्।"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G डेटा सीमा पुग्यो"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G डेटा सीमा पुग्यो"</string> diff --git a/core/res/res/values-nl/strings.xml b/core/res/res/values-nl/strings.xml index 33fcb8abcc20..69a407421aa1 100644 --- a/core/res/res/values-nl/strings.xml +++ b/core/res/res/values-nl/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB-drive"</string> <string name="storage_usb" msgid="3017954059538517278">"USB-opslag"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Bewerken"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Waarschuwing v. gegevensgebruik"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Tik voor gebruik en instellingen"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Gegevenslimiet van 2G-3G bereikt"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Gegevenslimiet van 4G bereikt"</string> diff --git a/core/res/res/values-pa-rIN/strings.xml b/core/res/res/values-pa-rIN/strings.xml index ae8a4b13cd03..ac80d5163f01 100644 --- a/core/res/res/values-pa-rIN/strings.xml +++ b/core/res/res/values-pa-rIN/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB ਡ੍ਰਾਇਵ"</string> <string name="storage_usb" msgid="3017954059538517278">"USB ਸਟੋਰੇਜ"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"ਸੰਪਾਦਿਤ ਕਰੋ"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"ਡੈਟਾ ਉਪਯੋਗ ਚਿਤਾਵਨੀ"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"ਵਰਤੋਂ ਅਤੇ ਸੈਟਿੰਗਾਂ ਨੂੰ ਵੇਖਣ ਲਈ ਟੈਪ ਕਰੋ।"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G ਡੈਟਾ ਸੀਮਾ ਪੂਰੀ ਹੋ ਗਈ"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G ਡੈਟਾ ਸੀਮਾ ਪੂਰੀ ਹੋਈ"</string> diff --git a/core/res/res/values-pl/strings.xml b/core/res/res/values-pl/strings.xml index 8a2dae5c0f40..86988460866c 100644 --- a/core/res/res/values-pl/strings.xml +++ b/core/res/res/values-pl/strings.xml @@ -1382,7 +1382,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Dysk USB (<xliff:g id="MANUFACTURER">%s</xliff:g>)"</string> <string name="storage_usb" msgid="3017954059538517278">"Nośnik USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Edytuj"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Ostrzeżenie o transmisji danych"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Kliknij, by wyświetlić użycie i ustawienia."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Osiągnięto limit danych 2G/3G"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Osiągnięto limit danych 4G"</string> diff --git a/core/res/res/values-pt-rBR/strings.xml b/core/res/res/values-pt-rBR/strings.xml index 191a41af593d..74b75193d0db 100644 --- a/core/res/res/values-pt-rBR/strings.xml +++ b/core/res/res/values-pt-rBR/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Drive USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Armazenamento USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Editar"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Aviso sobre uso de dados"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Toque para ver uso e config."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Limite de dados 2G-3G atingido"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Limite de dados 4G atingido"</string> diff --git a/core/res/res/values-pt-rPT/strings.xml b/core/res/res/values-pt-rPT/strings.xml index 9db6a76d5dd9..58e4097427cd 100644 --- a/core/res/res/values-pt-rPT/strings.xml +++ b/core/res/res/values-pt-rPT/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Unidade USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Armazenamento USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Editar"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Aviso de utilização de dados"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Toque para ver a utilização e definições"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Limite de dados 2G/3G atingido"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Limite de dados 4G atingido"</string> diff --git a/core/res/res/values-pt/strings.xml b/core/res/res/values-pt/strings.xml index 191a41af593d..74b75193d0db 100644 --- a/core/res/res/values-pt/strings.xml +++ b/core/res/res/values-pt/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Drive USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Armazenamento USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Editar"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Aviso sobre uso de dados"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Toque para ver uso e config."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Limite de dados 2G-3G atingido"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Limite de dados 4G atingido"</string> diff --git a/core/res/res/values-ro/strings.xml b/core/res/res/values-ro/strings.xml index 197da9683029..50113632abef 100644 --- a/core/res/res/values-ro/strings.xml +++ b/core/res/res/values-ro/strings.xml @@ -1356,7 +1356,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Unitate USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Dsipozitiv de stocare USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Editați"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Avertisment de utiliz. a datelor"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Atingeți ca să vedeți utilizarea/setările."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Ați atins limita de date 2G-3G"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Ați atins limita de date 4G"</string> diff --git a/core/res/res/values-ru/strings.xml b/core/res/res/values-ru/strings.xml index 11f19b8d2c67..7850cd8dc5df 100644 --- a/core/res/res/values-ru/strings.xml +++ b/core/res/res/values-ru/strings.xml @@ -1382,7 +1382,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"USB-накопитель <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"USB-накопитель"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Изменить"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Осталось мало трафика"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Нажмите, чтобы проверить трафик и настройки."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Достигнут лимит трафика 2G/3G"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Достигнут лимит трафика 4G"</string> diff --git a/core/res/res/values-si-rLK/strings.xml b/core/res/res/values-si-rLK/strings.xml index 73e28dcd9b16..7cab897a5269 100644 --- a/core/res/res/values-si-rLK/strings.xml +++ b/core/res/res/values-si-rLK/strings.xml @@ -1332,7 +1332,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB ධාවකය"</string> <string name="storage_usb" msgid="3017954059538517278">"USB ආචයනය"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"සංස්කරණය කරන්න"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"දත්ත භාවිතා අවවාදය"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"භාවිතය සහ සැකසීම් බැලීමට තට්ටු කරන්න."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G දත්ත සීමාවට ළඟාවී ඇත"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G දත්ත සීමාවට ළඟාවී ඇත"</string> diff --git a/core/res/res/values-sk/strings.xml b/core/res/res/values-sk/strings.xml index 2aa140add7d8..26e275f96e68 100644 --- a/core/res/res/values-sk/strings.xml +++ b/core/res/res/values-sk/strings.xml @@ -1382,7 +1382,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Disk USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Ukladací priestor USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Upraviť"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Upozornenie o využití dát"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Klepnutím zobrazíte využitie a nastavenia."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Bol dosiahnutý limit 2G–3G"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Bol dosiahnutý limit 4G"</string> diff --git a/core/res/res/values-sl/strings.xml b/core/res/res/values-sl/strings.xml index d942c46e928c..7f49968d56fc 100644 --- a/core/res/res/values-sl/strings.xml +++ b/core/res/res/values-sl/strings.xml @@ -1382,7 +1382,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Pogon USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Pomnilnik USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Uredi"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Opozorilo o uporabi podatkov"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Dot. se za ogled upor. in nast."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Dosežena pod. omejitev za 2G/3G"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Dosežena pod. omejitev za 4G"</string> diff --git a/core/res/res/values-sq-rAL/strings.xml b/core/res/res/values-sq-rAL/strings.xml index 26637b4afe5b..c906f74e5a8f 100644 --- a/core/res/res/values-sq-rAL/strings.xml +++ b/core/res/res/values-sq-rAL/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"USB-ja nga <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Hapësira ruajtëse e USB-së"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Redakto"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Paralajmërim për përdorimin e të dhënave"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Trokit për të parë përdorimin dhe cilësimet."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Kufiri i të dhënave 2G-3G u arrit"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Kufiri i të dhënave 4G u arrit"</string> diff --git a/core/res/res/values-sr/strings.xml b/core/res/res/values-sr/strings.xml index 162debe58771..f029dbb33674 100644 --- a/core/res/res/values-sr/strings.xml +++ b/core/res/res/values-sr/strings.xml @@ -1356,7 +1356,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB диск"</string> <string name="storage_usb" msgid="3017954059538517278">"USB меморија"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Измени"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Упозорење о потрошњи података"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Додирните за потрошњу и подешавања."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Нема више 2G-3G података"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Нема више 4G података"</string> diff --git a/core/res/res/values-sv/strings.xml b/core/res/res/values-sv/strings.xml index a0b6db357e5b..3f4f8362ccc5 100644 --- a/core/res/res/values-sv/strings.xml +++ b/core/res/res/values-sv/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"USB-enhet (<xliff:g id="MANUFACTURER">%s</xliff:g>)"</string> <string name="storage_usb" msgid="3017954059538517278">"USB-lagring"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Redigera"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Varning angående dataanvändning"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Visa användning och inställning."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Datagränsen för 2G-3G har uppnåtts"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Datagränsen för 4G har uppnåtts"</string> diff --git a/core/res/res/values-sw/strings.xml b/core/res/res/values-sw/strings.xml index 2f5c4f9df046..9cba3909248b 100644 --- a/core/res/res/values-sw/strings.xml +++ b/core/res/res/values-sw/strings.xml @@ -1328,7 +1328,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Hifadhi ya USB iliyotengenezwa na <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Hifadhi ya USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Badilisha"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Onyo la matumizi ya data"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Gonga ili uangalie matumizi na mipangilio."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Kikomo data ya 2G-3G kimefikiwa"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Kikomo cha data ya 4G kimefikiwa"</string> diff --git a/core/res/res/values-ta-rIN/strings.xml b/core/res/res/values-ta-rIN/strings.xml index 1cb406237506..e784a7648aef 100644 --- a/core/res/res/values-ta-rIN/strings.xml +++ b/core/res/res/values-ta-rIN/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB டிரைவ்"</string> <string name="storage_usb" msgid="3017954059538517278">"USB சேமிப்பிடம்"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"திருத்து"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"தரவு பயன்பாட்டு எச்சரிக்கை"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"தரவு உபயோகம், அமைப்புகளைப் பார்க்க, தட்டவும்."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G தரவு வரம்பைக் கடந்தது"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G தரவு வரம்பைக் கடந்தது"</string> diff --git a/core/res/res/values-te-rIN/strings.xml b/core/res/res/values-te-rIN/strings.xml index e9aa6ba621b3..21ed7c6fe84c 100644 --- a/core/res/res/values-te-rIN/strings.xml +++ b/core/res/res/values-te-rIN/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB డ్రైవ్"</string> <string name="storage_usb" msgid="3017954059538517278">"USB నిల్వ"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"సవరించు"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"డేటా వినియోగం హెచ్చరిక"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"వినియోగం,సెట్టింగ్ల కోసం నొక్కండి"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G డేటా పరిమితిని చేరుకుంది"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G డేటా పరిమితిని చేరుకుంది"</string> diff --git a/core/res/res/values-th/strings.xml b/core/res/res/values-th/strings.xml index f3445cb16c75..df9b18291d4b 100644 --- a/core/res/res/values-th/strings.xml +++ b/core/res/res/values-th/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"ไดรฟ์ USB ของ <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"ที่เก็บข้อมูล USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"แก้ไข"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"คำเตือนการใช้ข้อมูล"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"แตะเพื่อดูการใช้งานและการตั้งค่า"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"ถึงขีดจำกัดข้อมูล 2G-3G แล้ว"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"ถึงขีดจำกัดข้อมูล 4G แล้ว"</string> diff --git a/core/res/res/values-tl/strings.xml b/core/res/res/values-tl/strings.xml index ef1a57dfafe3..fac6eebc22c1 100644 --- a/core/res/res/values-tl/strings.xml +++ b/core/res/res/values-tl/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB drive"</string> <string name="storage_usb" msgid="3017954059538517278">"USB storage"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"I-edit"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Babala sa paggamit ng data"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"I-tap tingnan paggamit/setting."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Naabot na ang limitasyon sa 2G-3G data"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Naabot na ang limitasyon sa 4G data"</string> diff --git a/core/res/res/values-tr/strings.xml b/core/res/res/values-tr/strings.xml index daefd3ddcca3..afd9e68e1567 100644 --- a/core/res/res/values-tr/strings.xml +++ b/core/res/res/values-tr/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB sürücüsü"</string> <string name="storage_usb" msgid="3017954059538517278">"USB bellek"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Düzenle"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Veri kullanım uyarısı"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Kul. ve ayar. gör. için dokunun."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G veri sınırına ulaşıldı"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G veri sınırına ulaşıldı"</string> diff --git a/core/res/res/values-uk/strings.xml b/core/res/res/values-uk/strings.xml index 42ac19156595..0ce3396f28aa 100644 --- a/core/res/res/values-uk/strings.xml +++ b/core/res/res/values-uk/strings.xml @@ -1382,7 +1382,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Носій USB (<xliff:g id="MANUFACTURER">%s</xliff:g>)"</string> <string name="storage_usb" msgid="3017954059538517278">"Носій USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Редагувати"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Застереження про використ. даних"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Переглянути дані та параметри."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Досягнуто ліміту даних 2G–3G"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Досягнуто ліміту даних 4G"</string> diff --git a/core/res/res/values-ur-rPK/strings.xml b/core/res/res/values-ur-rPK/strings.xml index e351ec074d3a..ebab740c1761 100644 --- a/core/res/res/values-ur-rPK/strings.xml +++ b/core/res/res/values-ur-rPK/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB ڈرائیو"</string> <string name="storage_usb" msgid="3017954059538517278">"USB اسٹوریج"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"ترمیم کریں"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"ڈیٹا کے استعمال کی وارننگ"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"استعمال اور ترتیبات دیکھنے کیلئے تھپتھپائیں۔"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G ڈیٹا کی حد کو پہنچ گیا"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G ڈیٹا کی حد کو پہنچ گیا"</string> diff --git a/core/res/res/values-uz-rUZ/strings.xml b/core/res/res/values-uz-rUZ/strings.xml index d75291b2f8fd..f97a7b825395 100644 --- a/core/res/res/values-uz-rUZ/strings.xml +++ b/core/res/res/values-uz-rUZ/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB xotira qurilmasi"</string> <string name="storage_usb" msgid="3017954059538517278">"USB xotira"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Tahrirlash"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Trafik kam qoldi"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Trafik sarfi va sozlamalarni ko‘rish uchun bosing."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G trafik chekloviga yetdi"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G trafik chekloviga yetdi"</string> diff --git a/core/res/res/values-vi/strings.xml b/core/res/res/values-vi/strings.xml index 058999bffc01..66b87e7295c3 100644 --- a/core/res/res/values-vi/strings.xml +++ b/core/res/res/values-vi/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"Ổ USB <xliff:g id="MANUFACTURER">%s</xliff:g>"</string> <string name="storage_usb" msgid="3017954059538517278">"Bộ lưu trữ USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Chỉnh sửa"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Cảnh báo sử dụng dữ liệu"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Nhấn để xem sử dụng và cài đặt."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"Đã đạt tới giới hạn dữ liệu 2G-3G"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"Đã đạt tới giới hạn dữ liệu 4G"</string> diff --git a/core/res/res/values-zh-rCN/strings.xml b/core/res/res/values-zh-rCN/strings.xml index 5d7ddcb42f50..1bd10237dbf4 100644 --- a/core/res/res/values-zh-rCN/strings.xml +++ b/core/res/res/values-zh-rCN/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> U 盘"</string> <string name="storage_usb" msgid="3017954059538517278">"USB存储器"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"修改"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"流量警告"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"点按即可查看使用情况和设置。"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"已达到2G-3G流量上限"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"已达到4G流量上限"</string> diff --git a/core/res/res/values-zh-rHK/strings.xml b/core/res/res/values-zh-rHK/strings.xml index 25a0276d5fc0..4329c8fcca2a 100644 --- a/core/res/res/values-zh-rHK/strings.xml +++ b/core/res/res/values-zh-rHK/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB 驅動器"</string> <string name="storage_usb" msgid="3017954059538517278">"USB 儲存裝置"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"編輯"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"資料用量警告"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"輕按即可查看用量和設定。"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"已達到 2G-3G 數據流量上限"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"已達到 4G 數據流量上限"</string> diff --git a/core/res/res/values-zh-rTW/strings.xml b/core/res/res/values-zh-rTW/strings.xml index 3b1296ca023c..ec7a9a0807d1 100644 --- a/core/res/res/values-zh-rTW/strings.xml +++ b/core/res/res/values-zh-rTW/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> USB 隨身碟"</string> <string name="storage_usb" msgid="3017954059538517278">"USB 儲存裝置"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"編輯"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"數據用量警告"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"輕觸即可查看用量和設定。"</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"已達到 2G-3G 數據流量上限"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"已達到 4G 數據流量上限"</string> diff --git a/core/res/res/values-zu/strings.xml b/core/res/res/values-zu/strings.xml index b50e76ce80ed..3a1964838c51 100644 --- a/core/res/res/values-zu/strings.xml +++ b/core/res/res/values-zu/strings.xml @@ -1330,7 +1330,8 @@ <string name="storage_usb_drive_label" msgid="4501418548927759953">"<xliff:g id="MANUFACTURER">%s</xliff:g> idrayivu ye-USB"</string> <string name="storage_usb" msgid="3017954059538517278">"Isitoreji se-USB"</string> <string name="extract_edit_menu_button" msgid="8940478730496610137">"Hlela"</string> - <string name="data_usage_warning_title" msgid="1955638862122232342">"Isexwayiso sokusetshenziswa kwedatha"</string> + <!-- no translation found for data_usage_warning_title (3620440638180218181) --> + <skip /> <string name="data_usage_warning_body" msgid="6660692274311972007">"Thepha ukuze ubuke ukusetshenziswa nezilungiselelo."</string> <string name="data_usage_3g_limit_title" msgid="4361523876818447683">"2G-3G umkhawulo wedatha ufinyelelwe"</string> <string name="data_usage_4g_limit_title" msgid="4609566827219442376">"4G umkhawulo wedatha ufinyelelwe"</string> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 3ae618638209..85838edb53dd 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2234,6 +2234,11 @@ <!-- Flag specifying whether WFC over IMS is available on device --> <bool name="config_device_wfc_ims_available">false</bool> + <!-- Flag specifying whether WFC over IMS should be available for carrier: independent of + carrier provisioning. If false: hard disabled. If true: then depends on carrier + provisioning, availability etc --> + <bool name="config_carrier_wfc_ims_available">false</bool> + <bool name="config_networkSamplingWakesDevice">true</bool> <string-array translatable="false" name="config_cdma_home_system" /> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index bf7317c1b71a..cce02f299fd8 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -3561,7 +3561,7 @@ <!-- Button text for the edit menu in input method extract mode. [CHAR LIMIT=16] --> <string name="extract_edit_menu_button">Edit</string> - <!-- Notification title when data usage has exceeded warning threshold. [CHAR LIMIT=32] --> + <!-- Notification title when data usage has exceeded warning threshold. [CHAR LIMIT=50] --> <string name="data_usage_warning_title">Data usage alert</string> <!-- Notification body when data usage has exceeded warning threshold. [CHAR LIMIT=32] --> <string name="data_usage_warning_body">Tap to view usage and settings.</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 449acc66a845..62997f7771cc 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -347,8 +347,6 @@ <java-symbol type="integer" name="config_wifi_operating_voltage_mv" /> <java-symbol type="string" name="config_wifi_framework_sap_2G_channel_list" /> - <java-symbol type="bool" name="editable_voicemailnumber" /> - <java-symbol type="bool" name="config_wifi_framework_cellular_handover_enable_user_triggered_adjustment" /> <java-symbol type="integer" name="config_wifi_framework_associated_full_scan_tx_packet_threshold" /> <java-symbol type="integer" name="config_wifi_framework_associated_full_scan_rx_packet_threshold" /> @@ -2227,6 +2225,7 @@ <java-symbol type="bool" name="config_device_respects_hold_carrier_config" /> <java-symbol type="bool" name="config_carrier_vt_available" /> <java-symbol type="bool" name="config_device_wfc_ims_available" /> + <java-symbol type="bool" name="config_carrier_wfc_ims_available" /> <java-symbol type="attr" name="touchscreenBlocksFocus" /> <java-symbol type="layout" name="resolver_list_with_default" /> <java-symbol type="string" name="whichApplicationNamed" /> diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml index c234b6aa314b..ee7861313428 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -1329,6 +1329,11 @@ android:exported="true"> </activity> + <activity + android:name="android.print.mockservice.AddPrintersActivity" + android:exported="true"> + </activity> + </application> <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner" diff --git a/core/tests/coretests/res/xml/printservice.xml b/core/tests/coretests/res/xml/printservice.xml index abbebdae0d57..b105a0f0b785 100644 --- a/core/tests/coretests/res/xml/printservice.xml +++ b/core/tests/coretests/res/xml/printservice.xml @@ -17,4 +17,5 @@ --> <print-service xmlns:android="http://schemas.android.com/apk/res/android" - android:settingsActivity="android.print.mockservice.SettingsActivity"/> + android:settingsActivity="android.print.mockservice.SettingsActivity" + android:addPrintersActivity="android.print.mockservice.AddPrintersActivity" /> diff --git a/core/tests/coretests/src/android/print/BasePrintTest.java b/core/tests/coretests/src/android/print/BasePrintTest.java index 2c2ee8c2e52f..8ef806290230 100644 --- a/core/tests/coretests/src/android/print/BasePrintTest.java +++ b/core/tests/coretests/src/android/print/BasePrintTest.java @@ -16,6 +16,9 @@ package android.print; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doCallRealMethod; @@ -26,173 +29,134 @@ import android.annotation.NonNull; import android.app.Instrumentation; import android.content.Context; import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.content.res.Resources; import android.os.CancellationSignal; -import android.os.LocaleList; import android.os.ParcelFileDescriptor; import android.os.SystemClock; -import android.print.PrintAttributes; -import android.print.PrintDocumentAdapter; -import android.print.PrintManager; -import android.print.PrinterId; import android.print.mockservice.PrintServiceCallbacks; import android.print.mockservice.PrinterDiscoverySessionCallbacks; import android.print.mockservice.StubbablePrinterDiscoverySession; import android.printservice.CustomPrinterIconCallback; import android.printservice.PrintJob; import android.printservice.PrintService; -import android.test.InstrumentationTestCase; -import android.util.DisplayMetrics; - +import android.support.test.InstrumentationRegistry; +import android.support.test.uiautomator.UiDevice; +import android.support.test.rule.ActivityTestRule; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; import org.mockito.stubbing.Answer; -import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStreamReader; -import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.concurrent.TimeoutException; /** * This is the base class for print tests. */ -public abstract class BasePrintTest extends InstrumentationTestCase { - - private static final long OPERATION_TIMEOUT = 30000; +abstract class BasePrintTest { + protected static final long OPERATION_TIMEOUT = 30000; private static final String PM_CLEAR_SUCCESS_OUTPUT = "Success"; - private static final String COMMAND_LIST_ENABLED_IME_COMPONENTS = "ime list -s"; - private static final String COMMAND_PREFIX_ENABLE_IME = "ime enable "; - private static final String COMMAND_PREFIX_DISABLE_IME = "ime disable "; private static final int CURRENT_USER_ID = -2; // Mirrors UserHandle.USER_CURRENT - private PrintTestActivity mActivity; private android.print.PrintJob mPrintJob; - private LocaleList mOldLocale; - private CallCounter mStartCallCounter; private CallCounter mStartSessionCallCounter; - private String[] mEnabledImes; - - private String[] getEnabledImes() throws IOException { - List<String> imeList = new ArrayList<>(); - - ParcelFileDescriptor pfd = getInstrumentation().getUiAutomation() - .executeShellCommand(COMMAND_LIST_ENABLED_IME_COMPONENTS); - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(new FileInputStream(pfd.getFileDescriptor())))) { + private static Instrumentation sInstrumentation; + private static UiDevice sUiDevice; + + @Rule + public ActivityTestRule<PrintTestActivity> mActivityRule = + new ActivityTestRule<>(PrintTestActivity.class, false, true); + + /** + * {@link Runnable} that can throw and {@link Exception} + */ + interface Invokable { + /** + * Execute the invokable + * + * @throws Exception + */ + void run() throws Exception; + } - String line; - while ((line = reader.readLine()) != null) { - imeList.add(line); + /** + * Assert that the invokable throws an expectedException + * + * @param invokable The {@link Invokable} to run + * @param expectedClass The {@link Exception} that is supposed to be thrown + */ + void assertException(Invokable invokable, Class<? extends Exception> expectedClass) + throws Exception { + try { + invokable.run(); + } catch (Exception e) { + if (e.getClass().isAssignableFrom(expectedClass)) { + return; + } else { + throw e; } } - String[] imeArray = new String[imeList.size()]; - imeList.toArray(imeArray); - - return imeArray; + throw new AssertionError("No exception thrown"); } - private void disableImes() throws Exception { - mEnabledImes = getEnabledImes(); - for (String ime : mEnabledImes) { - String disableImeCommand = COMMAND_PREFIX_DISABLE_IME + ime; - runShellCommand(getInstrumentation(), disableImeCommand); - } + /** + * Return the UI device + * + * @return the UI device + */ + public UiDevice getUiDevice() { + return sUiDevice; } - private void enableImes() throws Exception { - for (String ime : mEnabledImes) { - String enableImeCommand = COMMAND_PREFIX_ENABLE_IME + ime; - runShellCommand(getInstrumentation(), enableImeCommand); - } - mEnabledImes = null; + protected static Instrumentation getInstrumentation() { + return sInstrumentation; } - @Override - protected void runTest() throws Throwable { - // Do nothing if the device does not support printing. - if (supportsPrinting()) { - super.runTest(); - } - } + @BeforeClass + public static void setUpClass() throws Exception { + sInstrumentation = InstrumentationRegistry.getInstrumentation(); + assumeTrue(sInstrumentation.getContext().getPackageManager().hasSystemFeature( + PackageManager.FEATURE_PRINTING)); - @Override - public void setUp() throws Exception { - super.setUp(); - if (!supportsPrinting()) { - return; - } + sUiDevice = UiDevice.getInstance(sInstrumentation); // Make sure we start with a clean slate. clearPrintSpoolerData(); - disableImes(); // Workaround for dexmaker bug: https://code.google.com/p/dexmaker/issues/detail?id=2 // Dexmaker is used by mockito. System.setProperty("dexmaker.dexcache", getInstrumentation() .getTargetContext().getCacheDir().getPath()); + } - // Set to US locale. - Resources resources = getInstrumentation().getTargetContext().getResources(); - Configuration oldConfiguration = resources.getConfiguration(); - if (!oldConfiguration.getLocales().get(0).equals(Locale.US)) { - mOldLocale = oldConfiguration.getLocales(); - DisplayMetrics displayMetrics = resources.getDisplayMetrics(); - Configuration newConfiguration = new Configuration(oldConfiguration); - newConfiguration.setLocale(Locale.US); - resources.updateConfiguration(newConfiguration, displayMetrics); - } - + @Before + public void initCounters() throws Exception { // Initialize the latches. mStartCallCounter = new CallCounter(); mStartSessionCallCounter = new CallCounter(); - - // Create the activity for the right locale. - createActivity(); } - @Override - public void tearDown() throws Exception { - if (!supportsPrinting()) { - return; - } - - // Done with the activity. - getActivity().finish(); - enableImes(); - - // Restore the locale if needed. - if (mOldLocale != null) { - Resources resources = getInstrumentation().getTargetContext().getResources(); - DisplayMetrics displayMetrics = resources.getDisplayMetrics(); - Configuration newConfiguration = new Configuration(resources.getConfiguration()); - newConfiguration.setLocales(mOldLocale); - mOldLocale = null; - resources.updateConfiguration(newConfiguration, displayMetrics); - } - - // Make sure the spooler is cleaned, this also un-approves all services - clearPrintSpoolerData(); - - super.tearDown(); + @After + public void exitActivities() throws Exception { + // Exit print spooler + getUiDevice().pressBack(); + getUiDevice().pressBack(); } protected android.print.PrintJob print(@NonNull final PrintDocumentAdapter adapter, final PrintAttributes attributes) { // Initiate printing as if coming from the app. - getInstrumentation().runOnMainSync(new Runnable() { - @Override - public void run() { - PrintManager printManager = (PrintManager) getActivity() - .getSystemService(Context.PRINT_SERVICE); - mPrintJob = printManager.print("Print job", adapter, attributes); - } + getInstrumentation().runOnMainSync(() -> { + PrintManager printManager = (PrintManager) getActivity() + .getSystemService(Context.PRINT_SERVICE); + mPrintJob = printManager.print("Print job", adapter, attributes); }); return mPrintJob; @@ -215,7 +179,7 @@ public abstract class BasePrintTest extends InstrumentationTestCase { waitForCallbackCallCount(mStartCallCounter, 1, "Did not get expected call to start."); } - private void waitForCallbackCallCount(CallCounter counter, int count, String message) { + private static void waitForCallbackCallCount(CallCounter counter, int count, String message) { try { counter.waitForCount(count, OPERATION_TIMEOUT); } catch (TimeoutException te) { @@ -224,12 +188,7 @@ public abstract class BasePrintTest extends InstrumentationTestCase { } protected PrintTestActivity getActivity() { - return mActivity; - } - - protected void createActivity() { - mActivity = launchActivity(getInstrumentation().getTargetContext().getPackageName(), - PrintTestActivity.class, null); + return mActivityRule.getActivity(); } public static String runShellCommand(Instrumentation instrumentation, String cmd) @@ -238,7 +197,7 @@ public abstract class BasePrintTest extends InstrumentationTestCase { byte[] buf = new byte[512]; int bytesRead; FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd); - StringBuffer stdout = new StringBuffer(); + StringBuilder stdout = new StringBuilder(); while ((bytesRead = fis.read(buf)) != -1) { stdout.append(new String(buf, 0, bytesRead)); } @@ -246,7 +205,7 @@ public abstract class BasePrintTest extends InstrumentationTestCase { return stdout.toString(); } - protected void clearPrintSpoolerData() throws Exception { + protected static void clearPrintSpoolerData() throws Exception { assertTrue("failed to clear print spooler data", runShellCommand(getInstrumentation(), String.format( "pm clear --user %d %s", CURRENT_USER_ID, @@ -319,7 +278,7 @@ public abstract class BasePrintTest extends InstrumentationTestCase { return service; } - protected final class CallCounter { + private static final class CallCounter { private final Object mLock = new Object(); private int mCallCount; @@ -331,7 +290,7 @@ public abstract class BasePrintTest extends InstrumentationTestCase { } } - public int getCallCount() { + int getCallCount() { synchronized (mLock) { return mCallCount; } @@ -355,9 +314,4 @@ public abstract class BasePrintTest extends InstrumentationTestCase { } } } - - protected boolean supportsPrinting() { - return getInstrumentation().getContext().getPackageManager() - .hasSystemFeature(PackageManager.FEATURE_PRINTING); - } } diff --git a/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java b/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java index d491ec4111fb..2e9c8e735fd9 100644 --- a/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java +++ b/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java @@ -38,17 +38,23 @@ import android.print.mockservice.PrintServiceCallbacks; import android.print.mockservice.PrinterDiscoverySessionCallbacks; import android.print.mockservice.StubbablePrinterDiscoverySession; -import android.test.suitebuilder.annotation.LargeTest; -import android.test.suitebuilder.annotation.MediumTest; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; +import android.support.test.filters.LargeTest; +import android.support.test.filters.MediumTest; +import android.support.test.runner.AndroidJUnit4; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.List; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + /** * tests feeding all possible parameters to the IPrintManager Binder. */ +@RunWith(AndroidJUnit4.class) public class IPrintManagerParametersTest extends BasePrintTest { private final int BAD_APP_ID = 0xffffffff; @@ -58,9 +64,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { private final PrintJobId mBadPrintJobId; private PrintJob mGoodPrintJob; - private PrinterId mBadPrinterId; private PrinterId mGoodPrinterId; - private ComponentName mGoodComponentName; private ComponentName mBadComponentName; private IPrintManager mIPrintManager; @@ -93,6 +97,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, CancellationSignal cancellationSignal, LayoutResultCallback callback, Bundle extras) { + callback.onLayoutFailed("not implemented"); } @Override @@ -109,54 +114,46 @@ public class IPrintManagerParametersTest extends BasePrintTest { */ private PrintServiceCallbacks createMockCallbacks() { return createMockPrintServiceCallbacks( - new Answer<PrinterDiscoverySessionCallbacks>() { - @Override - public PrinterDiscoverySessionCallbacks answer(InvocationOnMock invocation) { - return createMockPrinterDiscoverySessionCallbacks(new Answer<Void>() { - @Override - public Void answer(InvocationOnMock invocation) { - // Get the session. - StubbablePrinterDiscoverySession session = - ((PrinterDiscoverySessionCallbacks) invocation - .getMock()).getSession(); - - if (session.getPrinters().isEmpty()) { - final String PRINTER_NAME = "good printer"; - List<PrinterInfo> printers = new ArrayList<>(); - - // Add the printer. - mGoodPrinterId = session.getService() - .generatePrinterId(PRINTER_NAME); - - PrinterCapabilitiesInfo capabilities = - new PrinterCapabilitiesInfo.Builder(mGoodPrinterId) - .setMinMargins( - new Margins(200, 200, 200, 200)) - .addMediaSize(MediaSize.ISO_A4, true) - .addResolution(new Resolution("300x300", - "300x300", 300, 300), - true) - .setColorModes( - PrintAttributes.COLOR_MODE_COLOR, - PrintAttributes.COLOR_MODE_COLOR) - .build(); - - PrinterInfo printer = new PrinterInfo.Builder( - mGoodPrinterId, - PRINTER_NAME, - PrinterInfo.STATUS_IDLE) - .setCapabilities(capabilities) - .build(); - printers.add(printer); - - session.addPrinters(printers); - } - onPrinterDiscoverySessionStartCalled(); - return null; - } - }, null, null, null, null, null, null); + invocation -> createMockPrinterDiscoverySessionCallbacks(invocation1 -> { + // Get the session. + StubbablePrinterDiscoverySession session = + ((PrinterDiscoverySessionCallbacks) invocation1 + .getMock()).getSession(); + + if (session.getPrinters().isEmpty()) { + final String PRINTER_NAME = "good printer"; + List<PrinterInfo> printers = new ArrayList<>(); + + // Add the printer. + mGoodPrinterId = session.getService() + .generatePrinterId(PRINTER_NAME); + + PrinterCapabilitiesInfo capabilities = + new PrinterCapabilitiesInfo.Builder(mGoodPrinterId) + .setMinMargins( + new Margins(200, 200, 200, 200)) + .addMediaSize(MediaSize.ISO_A4, true) + .addResolution(new Resolution("300x300", + "300x300", 300, 300), + true) + .setColorModes( + PrintAttributes.COLOR_MODE_COLOR, + PrintAttributes.COLOR_MODE_COLOR) + .build(); + + PrinterInfo printer = new PrinterInfo.Builder( + mGoodPrinterId, + PRINTER_NAME, + PrinterInfo.STATUS_IDLE) + .setCapabilities(capabilities) + .build(); + printers.add(printer); + + session.addPrinters(printers); } - }, + onPrinterDiscoverySessionStartCalled(); + return null; + }, null, null, null, null, null, null), null, null); } @@ -214,60 +211,28 @@ public class IPrintManagerParametersTest extends BasePrintTest { waitForPrinterDiscoverySessionStartCallbackCalled(); } - @Override - public void setUp() throws Exception { - super.setUp(); - - MockPrintService.setCallbacks(createMockCallbacks()); - - mGoodComponentName = getActivity().getComponentName(); - - mIPrintManager = IPrintManager.Stub - .asInterface(ServiceManager.getService(Context.PRINT_SERVICE)); - - // Generate dummy printerId which is a valid PrinterId object, but does not correspond to a - // printer - mBadPrinterId = new PrinterId(mGoodComponentName, "dummy printer"); - } - /** - * {@link Runnable} that can throw and {@link Exception} + * Return a printer Id that is not from any print service + * + * @return The bad printer id. */ - private interface Invokable { - /** - * Execute the invokable - * - * @throws Exception - */ - void run() throws Exception; + private PrinterId getBadPrinterId() { + return new PrinterId(getActivity().getComponentName(), "dummy printer"); } - /** - * Assert that the invokable throws an expectedException - * - * @param invokable The {@link Invokable} to run - * @param expectedClass The {@link Exception} that is supposed to be thrown - */ - public void assertException(Invokable invokable, Class<? extends Exception> expectedClass) - throws Exception { - try { - invokable.run(); - } catch (Exception e) { - if (e.getClass().isAssignableFrom(expectedClass)) { - return; - } else { - throw new AssertionError("Expected: " + expectedClass.getName() + ", got: " - + e.getClass().getName()); - } - } + @Before + public void setUpMockService() throws Exception { + MockPrintService.setCallbacks(createMockCallbacks()); - throw new AssertionError("No exception thrown"); + mIPrintManager = IPrintManager.Stub + .asInterface(ServiceManager.getService(Context.PRINT_SERVICE)); } /** * test IPrintManager.getPrintJobInfo */ @LargeTest + @Test public void testGetPrintJobInfo() throws Exception { startPrinting(); @@ -276,12 +241,9 @@ public class IPrintManagerParametersTest extends BasePrintTest { assertEquals(null, mIPrintManager.getPrintJobInfo(mBadPrintJobId, mAppId, mUserId)); assertEquals(null, mIPrintManager.getPrintJobInfo(null, mAppId, mUserId)); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.getPrintJobInfo(mGoodPrintJob.getId(), BAD_APP_ID, mUserId); - } - }, SecurityException.class); + assertException( + () -> mIPrintManager.getPrintJobInfo(mGoodPrintJob.getId(), BAD_APP_ID, mUserId), + SecurityException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -290,6 +252,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.getPrintJobInfos */ @LargeTest + @Test public void testGetPrintJobInfos() throws Exception { startPrinting(); @@ -304,12 +267,8 @@ public class IPrintManagerParametersTest extends BasePrintTest { } assertTrue(foundPrintJob); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.getPrintJobInfos(BAD_APP_ID, mUserId); - } - }, SecurityException.class); + assertException(() -> mIPrintManager.getPrintJobInfos(BAD_APP_ID, mUserId), + SecurityException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -318,6 +277,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.print */ @LargeTest + @Test public void testPrint() throws Exception { final String name = "dummy print job"; @@ -326,44 +286,23 @@ public class IPrintManagerParametersTest extends BasePrintTest { startPrinting(); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.print(null, adapter, null, mGoodComponentName.getPackageName(), - mAppId, mUserId); - } - }, IllegalArgumentException.class); + assertException(() -> mIPrintManager.print(null, adapter, null, + getActivity().getPackageName(), mAppId, mUserId), + IllegalArgumentException.class); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.print(name, null, null, mGoodComponentName.getPackageName(), - mAppId, mUserId); - } - }, NullPointerException.class); + assertException(() -> mIPrintManager.print(name, null, null, + getActivity().getPackageName(), mAppId, mUserId), + NullPointerException.class); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.print(name, adapter, null, null, mAppId, mUserId); - } - }, IllegalArgumentException.class); + assertException(() -> mIPrintManager.print(name, adapter, null, null, mAppId, mUserId), + IllegalArgumentException.class); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.print(name, adapter, null, mBadComponentName.getPackageName(), - mAppId, mUserId); - } - }, IllegalArgumentException.class); + assertException(() -> mIPrintManager.print(name, adapter, null, + mBadComponentName.getPackageName(), mAppId, mUserId), + IllegalArgumentException.class); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.print(name, adapter, null, mGoodComponentName.getPackageName(), - BAD_APP_ID, mUserId); - } - }, SecurityException.class); + assertException(() -> mIPrintManager.print(name, adapter, null, + getActivity().getPackageName(), BAD_APP_ID, mUserId), SecurityException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -372,6 +311,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.cancelPrintJob */ @LargeTest + @Test public void testCancelPrintJob() throws Exception { startPrinting(); @@ -379,12 +319,9 @@ public class IPrintManagerParametersTest extends BasePrintTest { mIPrintManager.cancelPrintJob(mBadPrintJobId, mAppId, mUserId); mIPrintManager.cancelPrintJob(null, mAppId, mUserId); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.cancelPrintJob(mGoodPrintJob.getId(), BAD_APP_ID, mUserId); - } - }, SecurityException.class); + assertException( + () -> mIPrintManager.cancelPrintJob(mGoodPrintJob.getId(), BAD_APP_ID, mUserId), + SecurityException.class); // Cannot test bad user Id as these tests are allowed to call across users @@ -396,6 +333,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.restartPrintJob */ @LargeTest + @Test public void testRestartPrintJob() throws Exception { startPrinting(); @@ -405,12 +343,9 @@ public class IPrintManagerParametersTest extends BasePrintTest { mIPrintManager.restartPrintJob(mBadPrintJobId, mAppId, mUserId); mIPrintManager.restartPrintJob(null, mAppId, mUserId); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.restartPrintJob(mGoodPrintJob.getId(), BAD_APP_ID, mUserId); - } - }, SecurityException.class); + assertException( + () -> mIPrintManager.restartPrintJob(mGoodPrintJob.getId(), BAD_APP_ID, mUserId), + SecurityException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -419,24 +354,18 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.addPrintJobStateChangeListener */ @MediumTest + @Test public void testAddPrintJobStateChangeListener() throws Exception { final IPrintJobStateChangeListener listener = createMockIPrintJobStateChangeListener(); mIPrintManager.addPrintJobStateChangeListener(listener, mAppId, mUserId); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.addPrintJobStateChangeListener(null, mAppId, mUserId); - } - }, NullPointerException.class); + assertException(() -> mIPrintManager.addPrintJobStateChangeListener(null, mAppId, mUserId), + NullPointerException.class); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.addPrintJobStateChangeListener(listener, BAD_APP_ID, mUserId); - } - }, SecurityException.class); + assertException( + () -> mIPrintManager.addPrintJobStateChangeListener(listener, BAD_APP_ID, mUserId), + SecurityException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -445,6 +374,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.removePrintJobStateChangeListener */ @MediumTest + @Test public void testRemovePrintJobStateChangeListener() throws Exception { final IPrintJobStateChangeListener listener = createMockIPrintJobStateChangeListener(); @@ -455,12 +385,8 @@ public class IPrintManagerParametersTest extends BasePrintTest { mIPrintManager.removePrintJobStateChangeListener(listener, mUserId); mIPrintManager.addPrintJobStateChangeListener(listener, mAppId, mUserId); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.removePrintJobStateChangeListener(null, mUserId); - } - }, NullPointerException.class); + assertException(() -> mIPrintManager.removePrintJobStateChangeListener(null, mUserId), + NullPointerException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -469,17 +395,14 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.addPrintServicesChangeListener */ @MediumTest + @Test public void testAddPrintServicesChangeListener() throws Exception { final IPrintServicesChangeListener listener = createMockIPrintServicesChangeListener(); mIPrintManager.addPrintServicesChangeListener(listener, mUserId); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.addPrintServicesChangeListener(null, mUserId); - } - }, NullPointerException.class); + assertException(() -> mIPrintManager.addPrintServicesChangeListener(null, mUserId), + NullPointerException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -488,6 +411,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.removePrintServicesChangeListener */ @MediumTest + @Test public void testRemovePrintServicesChangeListener() throws Exception { final IPrintServicesChangeListener listener = createMockIPrintServicesChangeListener(); @@ -498,12 +422,8 @@ public class IPrintManagerParametersTest extends BasePrintTest { mIPrintManager.removePrintServicesChangeListener(listener, mUserId); mIPrintManager.addPrintServicesChangeListener(listener, mUserId); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.removePrintServicesChangeListener(null, mUserId); - } - }, NullPointerException.class); + assertException(() -> mIPrintManager.removePrintServicesChangeListener(null, mUserId), + NullPointerException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -512,6 +432,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.getPrintServices */ @MediumTest + @Test public void testGetPrintServices() throws Exception { List<PrintServiceInfo> printServices = mIPrintManager.getPrintServices( PrintManager.ALL_SERVICES, mUserId); @@ -520,12 +441,8 @@ public class IPrintManagerParametersTest extends BasePrintTest { printServices = mIPrintManager.getPrintServices(0, mUserId); assertEquals(printServices, null); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.getPrintServices(~PrintManager.ALL_SERVICES, mUserId); - } - }, IllegalArgumentException.class); + assertException(() -> mIPrintManager.getPrintServices(~PrintManager.ALL_SERVICES, mUserId), + IllegalArgumentException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -534,38 +451,23 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.setPrintServiceEnabled */ @MediumTest + @Test public void testSetPrintServiceEnabled() throws Exception { final ComponentName printService = mIPrintManager.getPrintServices( PrintManager.ALL_SERVICES, mUserId).get(0).getComponentName(); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.setPrintServiceEnabled(printService, false, mUserId); - } - }, SecurityException.class); + assertException(() -> mIPrintManager.setPrintServiceEnabled(printService, false, mUserId), + SecurityException.class); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.setPrintServiceEnabled(printService, true, mUserId); - } - }, SecurityException.class); + assertException(() -> mIPrintManager.setPrintServiceEnabled(printService, true, mUserId), + SecurityException.class); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.setPrintServiceEnabled(new ComponentName("bad", "name"), true, - mUserId); - } - }, SecurityException.class); + assertException( + () -> mIPrintManager.setPrintServiceEnabled(new ComponentName("bad", "name"), true, + mUserId), SecurityException.class); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.setPrintServiceEnabled(null, true, mUserId); - } - }, SecurityException.class); + assertException(() -> mIPrintManager.setPrintServiceEnabled(null, true, mUserId), + SecurityException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -574,18 +476,16 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.addPrintServiceRecommendationsChangeListener */ @MediumTest + @Test public void testAddPrintServiceRecommendationsChangeListener() throws Exception { final IRecommendationsChangeListener listener = createMockIPrintServiceRecommendationsChangeListener(); mIPrintManager.addPrintServiceRecommendationsChangeListener(listener, mUserId); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.addPrintServiceRecommendationsChangeListener(null, mUserId); - } - }, NullPointerException.class); + assertException( + () -> mIPrintManager.addPrintServiceRecommendationsChangeListener(null, mUserId), + NullPointerException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -594,6 +494,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.removePrintServicesChangeListener */ @MediumTest + @Test public void testRemovePrintServiceRecommendationsChangeListener() throws Exception { final IRecommendationsChangeListener listener = createMockIPrintServiceRecommendationsChangeListener(); @@ -605,12 +506,9 @@ public class IPrintManagerParametersTest extends BasePrintTest { mIPrintManager.removePrintServiceRecommendationsChangeListener(listener, mUserId); mIPrintManager.addPrintServiceRecommendationsChangeListener(listener, mUserId); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.removePrintServiceRecommendationsChangeListener(null, mUserId); - } - }, NullPointerException.class); + assertException( + () -> mIPrintManager.removePrintServiceRecommendationsChangeListener(null, mUserId), + NullPointerException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -619,6 +517,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.getPrintServiceRecommendations */ @MediumTest + @Test public void testGetPrintServiceRecommendations() throws Exception { mIPrintManager.getPrintServiceRecommendations(mUserId); @@ -629,18 +528,15 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.createPrinterDiscoverySession */ @MediumTest + @Test public void testCreatePrinterDiscoverySession() throws Exception { final IPrinterDiscoveryObserver listener = createMockIPrinterDiscoveryObserver(); mIPrintManager.createPrinterDiscoverySession(listener, mUserId); try { - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.createPrinterDiscoverySession(null, mUserId); - } - }, NullPointerException.class); + assertException(() -> mIPrintManager.createPrinterDiscoverySession(null, mUserId), + NullPointerException.class); // Cannot test bad user Id as these tests are allowed to call across users } finally { @@ -655,6 +551,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.startPrinterDiscovery */ @LargeTest + @Test public void testStartPrinterDiscovery() throws Exception { startPrinting(); @@ -663,7 +560,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { goodPrinters.add(mGoodPrinterId); final List<PrinterId> badPrinters = new ArrayList<>(); - badPrinters.add(mBadPrinterId); + badPrinters.add(getBadPrinterId()); final List<PrinterId> emptyPrinters = new ArrayList<>(); @@ -677,19 +574,11 @@ public class IPrintManagerParametersTest extends BasePrintTest { mIPrintManager.startPrinterDiscovery(listener, emptyPrinters, mUserId); mIPrintManager.startPrinterDiscovery(listener, null, mUserId); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.startPrinterDiscovery(listener, nullPrinters, mUserId); - } - }, NullPointerException.class); + assertException(() -> mIPrintManager.startPrinterDiscovery(listener, nullPrinters, mUserId), + NullPointerException.class); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.startPrinterDiscovery(null, goodPrinters, mUserId); - } - }, NullPointerException.class); + assertException(() -> mIPrintManager.startPrinterDiscovery(null, goodPrinters, mUserId), + NullPointerException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -698,6 +587,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.stopPrinterDiscovery */ @MediumTest + @Test public void testStopPrinterDiscovery() throws Exception { final IPrinterDiscoveryObserver listener = createMockIPrinterDiscoveryObserver(); @@ -708,12 +598,8 @@ public class IPrintManagerParametersTest extends BasePrintTest { mIPrintManager.stopPrinterDiscovery(listener, mUserId); mIPrintManager.startPrinterDiscovery(listener, null, mUserId); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.stopPrinterDiscovery(null, mUserId); - } - }, NullPointerException.class); + assertException(() -> mIPrintManager.stopPrinterDiscovery(null, mUserId), + NullPointerException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -722,6 +608,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.validatePrinters */ @LargeTest + @Test public void testValidatePrinters() throws Exception { startPrinting(); @@ -729,7 +616,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { goodPrinters.add(mGoodPrinterId); final List<PrinterId> badPrinters = new ArrayList<>(); - badPrinters.add(mBadPrinterId); + badPrinters.add(getBadPrinterId()); final List<PrinterId> emptyPrinters = new ArrayList<>(); @@ -742,19 +629,11 @@ public class IPrintManagerParametersTest extends BasePrintTest { mIPrintManager.validatePrinters(badPrinters, mUserId); mIPrintManager.validatePrinters(emptyPrinters, mUserId); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.validatePrinters(null, mUserId); - } - }, NullPointerException.class); + assertException(() -> mIPrintManager.validatePrinters(null, mUserId), + NullPointerException.class); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.validatePrinters(nullPrinters, mUserId); - } - }, NullPointerException.class); + assertException(() -> mIPrintManager.validatePrinters(nullPrinters, mUserId), + NullPointerException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -763,20 +642,17 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.startPrinterStateTracking */ @LargeTest + @Test public void testStartPrinterStateTracking() throws Exception { startPrinting(); mIPrintManager.startPrinterStateTracking(mGoodPrinterId, mUserId); // Bad printers do no cause exceptions - mIPrintManager.startPrinterStateTracking(mBadPrinterId, mUserId); + mIPrintManager.startPrinterStateTracking(getBadPrinterId(), mUserId); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.startPrinterStateTracking(null, mUserId); - } - }, NullPointerException.class); + assertException(() -> mIPrintManager.startPrinterStateTracking(null, mUserId), + NullPointerException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -785,20 +661,17 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.getCustomPrinterIcon */ @LargeTest + @Test public void testGetCustomPrinterIcon() throws Exception { startPrinting(); mIPrintManager.getCustomPrinterIcon(mGoodPrinterId, mUserId); // Bad printers do no cause exceptions - mIPrintManager.getCustomPrinterIcon(mBadPrinterId, mUserId); + mIPrintManager.getCustomPrinterIcon(getBadPrinterId(), mUserId); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.getCustomPrinterIcon(null, mUserId); - } - }, NullPointerException.class); + assertException(() -> mIPrintManager.getCustomPrinterIcon(null, mUserId), + NullPointerException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -807,6 +680,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.stopPrinterStateTracking */ @LargeTest + @Test public void testStopPrinterStateTracking() throws Exception { startPrinting(); @@ -817,15 +691,11 @@ public class IPrintManagerParametersTest extends BasePrintTest { mIPrintManager.stopPrinterStateTracking(mGoodPrinterId, mUserId); // Bad printers do no cause exceptions - mIPrintManager.startPrinterStateTracking(mBadPrinterId, mUserId); - mIPrintManager.stopPrinterStateTracking(mBadPrinterId, mUserId); + mIPrintManager.startPrinterStateTracking(getBadPrinterId(), mUserId); + mIPrintManager.stopPrinterStateTracking(getBadPrinterId(), mUserId); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.stopPrinterStateTracking(null, mUserId); - } - }, NullPointerException.class); + assertException(() -> mIPrintManager.stopPrinterStateTracking(null, mUserId), + NullPointerException.class); // Cannot test bad user Id as these tests are allowed to call across users } @@ -834,6 +704,7 @@ public class IPrintManagerParametersTest extends BasePrintTest { * test IPrintManager.destroyPrinterDiscoverySession */ @MediumTest + @Test public void testDestroyPrinterDiscoverySession() throws Exception { final IPrinterDiscoveryObserver listener = createMockIPrinterDiscoveryObserver(); @@ -843,12 +714,8 @@ public class IPrintManagerParametersTest extends BasePrintTest { // Destroying already destroyed session is a no-op mIPrintManager.destroyPrinterDiscoverySession(listener, mUserId); - assertException(new Invokable() { - @Override - public void run() throws Exception { - mIPrintManager.destroyPrinterDiscoverySession(null, mUserId); - } - }, NullPointerException.class); + assertException(() -> mIPrintManager.destroyPrinterDiscoverySession(null, mUserId), + NullPointerException.class); // Cannot test bad user Id as these tests are allowed to call across users } diff --git a/core/tests/coretests/src/android/print/PrintTestActivity.java b/core/tests/coretests/src/android/print/PrintTestActivity.java index 86074a62eab6..e9b001f3c821 100644 --- a/core/tests/coretests/src/android/print/PrintTestActivity.java +++ b/core/tests/coretests/src/android/print/PrintTestActivity.java @@ -21,7 +21,6 @@ import android.os.Bundle; import android.view.WindowManager; public class PrintTestActivity extends Activity { - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/core/tests/coretests/src/android/print/WorkflowTest.java b/core/tests/coretests/src/android/print/WorkflowTest.java new file mode 100644 index 000000000000..35cfe223a6bf --- /dev/null +++ b/core/tests/coretests/src/android/print/WorkflowTest.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.print; + +import android.graphics.pdf.PdfDocument; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.print.mockservice.AddPrintersActivity; +import android.print.mockservice.MockPrintService; + +import android.print.mockservice.PrinterDiscoverySessionCallbacks; +import android.print.mockservice.StubbablePrinterDiscoverySession; +import android.print.pdf.PrintedPdfDocument; +import android.support.test.filters.LargeTest; +import android.support.test.uiautomator.UiObject; +import android.support.test.uiautomator.UiObjectNotFoundException; +import android.support.test.uiautomator.UiSelector; +import android.util.Log; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; + +import static org.junit.Assert.assertEquals; + +/** + * Tests for the basic printing workflows + */ +public class WorkflowTest extends BasePrintTest { + private static final String LOG_TAG = WorkflowTest.class.getSimpleName(); + + private static float sWindowAnimationScaleBefore; + private static float sTransitionAnimationScaleBefore; + private static float sAnimatiorDurationScaleBefore; + + interface InterruptableConsumer<T> { + void accept(T t) throws InterruptedException; + } + + /** + * Execute {@code waiter} until {@code condition} is met. + * + * @param condition Conditions to wait for + * @param waiter Code to execute while waiting + */ + private void waitWithTimeout(Supplier<Boolean> condition, InterruptableConsumer<Long> waiter) + throws TimeoutException, InterruptedException { + long startTime = System.currentTimeMillis(); + while (condition.get()) { + long timeLeft = OPERATION_TIMEOUT - (System.currentTimeMillis() - startTime); + if (timeLeft < 0) { + throw new TimeoutException(); + } + + waiter.accept(timeLeft); + } + } + + /** + * Executes a shell command using shell user identity, and return the standard output in + * string. + * + * @param cmd the command to run + * + * @return the standard output of the command + */ + private static String runShellCommand(String cmd) throws IOException { + try (FileInputStream is = new ParcelFileDescriptor.AutoCloseInputStream( + getInstrumentation().getUiAutomation().executeShellCommand(cmd))) { + byte[] buf = new byte[64]; + int bytesRead; + + StringBuilder stdout = new StringBuilder(); + while ((bytesRead = is.read(buf)) != -1) { + stdout.append(new String(buf, 0, bytesRead)); + } + + return stdout.toString(); + } + } + + @BeforeClass + public static void disableAnimations() throws Exception { + try { + sWindowAnimationScaleBefore = Float.parseFloat(runShellCommand( + "settings get global window_animation_scale")); + + runShellCommand("settings put global window_animation_scale 0"); + } catch (NumberFormatException e) { + sWindowAnimationScaleBefore = Float.NaN; + } + try { + sTransitionAnimationScaleBefore = Float.parseFloat(runShellCommand( + "settings get global transition_animation_scale")); + + runShellCommand("settings put global transition_animation_scale 0"); + } catch (NumberFormatException e) { + sTransitionAnimationScaleBefore = Float.NaN; + } + try { + sAnimatiorDurationScaleBefore = Float.parseFloat(runShellCommand( + "settings get global animator_duration_scale")); + + runShellCommand("settings put global animator_duration_scale 0"); + } catch (NumberFormatException e) { + sAnimatiorDurationScaleBefore = Float.NaN; + } + } + + @AfterClass + public static void enableAnimations() throws Exception { + if (sWindowAnimationScaleBefore != Float.NaN) { + runShellCommand( + "settings put global window_animation_scale " + sWindowAnimationScaleBefore); + } + if (sTransitionAnimationScaleBefore != Float.NaN) { + runShellCommand( + "settings put global transition_animation_scale " + + sTransitionAnimationScaleBefore); + } + if (sAnimatiorDurationScaleBefore != Float.NaN) { + runShellCommand( + "settings put global animator_duration_scale " + sAnimatiorDurationScaleBefore); + } + } + + /** Add a printer with a given name and supported mediasize to a session */ + private void addPrinter(StubbablePrinterDiscoverySession session, + String name, PrintAttributes.MediaSize mediaSize) { + PrinterId printerId = session.getService().generatePrinterId(name); + List<PrinterInfo> printers = new ArrayList<>(1); + + PrinterCapabilitiesInfo.Builder builder = + new PrinterCapabilitiesInfo.Builder(printerId); + + builder.setMinMargins(new PrintAttributes.Margins(0, 0, 0, 0)) + .setColorModes(PrintAttributes.COLOR_MODE_COLOR, + PrintAttributes.COLOR_MODE_COLOR) + .addMediaSize(mediaSize, true) + .addResolution(new PrintAttributes.Resolution("300x300", "300x300", 300, 300), + true); + + printers.add(new PrinterInfo.Builder(printerId, name, + PrinterInfo.STATUS_IDLE).setCapabilities(builder.build()).build()); + + session.addPrinters(printers); + } + + /** Find a certain element in the UI and click on it */ + private void clickOn(UiSelector selector) throws UiObjectNotFoundException { + Log.i(LOG_TAG, "Click on " + selector); + UiObject view = getUiDevice().findObject(selector); + view.click(); + getUiDevice().waitForIdle(); + } + + /** Find a certain text in the UI and click on it */ + private void clickOnText(String text) throws UiObjectNotFoundException { + clickOn(new UiSelector().text(text)); + } + + /** Set the printer in the print activity */ + private void setPrinter(String printerName) throws UiObjectNotFoundException { + clickOn(new UiSelector().resourceId("com.android.printspooler:id/destination_spinner")); + + clickOnText(printerName); + } + + /** + * Init mock print servic that returns a single printer by default. + * + * @param sessionRef Where to store the reference to the session once started + */ + private void setMockPrintServiceCallbacks(StubbablePrinterDiscoverySession[] sessionRef) { + MockPrintService.setCallbacks(createMockPrintServiceCallbacks( + inv -> createMockPrinterDiscoverySessionCallbacks(inv2 -> { + synchronized (sessionRef) { + sessionRef[0] = ((PrinterDiscoverySessionCallbacks) inv2.getMock()) + .getSession(); + + addPrinter(sessionRef[0], "1st printer", + PrintAttributes.MediaSize.ISO_A0); + + sessionRef.notifyAll(); + } + return null; + }, + null, null, null, null, null, inv2 -> { + synchronized (sessionRef) { + sessionRef[0] = null; + sessionRef.notifyAll(); + } + return null; + } + ), null, null)); + } + + /** + * Start print operation that just prints a single empty page + * + * @param printAttributesRef Where to store the reference to the print attributes once started + */ + private void print(PrintAttributes[] printAttributesRef) { + print(new PrintDocumentAdapter() { + @Override + public void onStart() { + } + + @Override + public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, + CancellationSignal cancellationSignal, LayoutResultCallback callback, + Bundle extras) { + callback.onLayoutFinished((new PrintDocumentInfo.Builder("doc")).build(), + !newAttributes.equals(printAttributesRef[0])); + + synchronized (printAttributesRef) { + printAttributesRef[0] = newAttributes; + printAttributesRef.notifyAll(); + } + } + + @Override + public void onWrite(PageRange[] pages, ParcelFileDescriptor destination, + CancellationSignal cancellationSignal, WriteResultCallback callback) { + try { + try { + PrintedPdfDocument document = new PrintedPdfDocument(getActivity(), + printAttributesRef[0]); + try { + PdfDocument.Page page = document.startPage(0); + document.finishPage(page); + try (FileOutputStream os = new FileOutputStream( + destination.getFileDescriptor())) { + document.writeTo(os); + os.flush(); + } + } finally { + document.close(); + } + } finally { + destination.close(); + } + + callback.onWriteFinished(pages); + } catch (IOException e) { + callback.onWriteFailed(e.getMessage()); + } + } + }, null); + } + + @Test + @LargeTest + public void addAndSelectPrinter() throws Exception { + final StubbablePrinterDiscoverySession session[] = new StubbablePrinterDiscoverySession[1]; + final PrintAttributes printAttributes[] = new PrintAttributes[1]; + + setMockPrintServiceCallbacks(session); + print(printAttributes); + + // We are now in the PrintActivity + Log.i(LOG_TAG, "Waiting for session"); + synchronized (session) { + waitWithTimeout(() -> session[0] == null, session::wait); + } + + setPrinter("1st printer"); + + Log.i(LOG_TAG, "Waiting for print attributes to change"); + synchronized (printAttributes) { + waitWithTimeout( + () -> printAttributes[0] == null || !printAttributes[0].getMediaSize().equals( + PrintAttributes.MediaSize.ISO_A0), printAttributes::wait); + } + + setPrinter("All printers\u2026"); + + // We are now in the SelectPrinterActivity + clickOnText("Add printer"); + + // We are now in the AddPrinterActivity + AddPrintersActivity.addObserver( + () -> addPrinter(session[0], "2nd printer", PrintAttributes.MediaSize.ISO_A1)); + + // This executes the observer registered above + clickOn(new UiSelector().text(MockPrintService.class.getCanonicalName()) + .resourceId("com.android.printspooler:id/title")); + + getUiDevice().pressBack(); + AddPrintersActivity.clearObservers(); + + // We are now in the SelectPrinterActivity + clickOnText("2nd printer"); + + // We are now in the PrintActivity + Log.i(LOG_TAG, "Waiting for print attributes to change"); + synchronized (printAttributes) { + waitWithTimeout( + () -> printAttributes[0] == null || !printAttributes[0].getMediaSize().equals( + PrintAttributes.MediaSize.ISO_A1), printAttributes::wait); + } + + getUiDevice().pressBack(); + + // We are back in the test activity + Log.i(LOG_TAG, "Waiting for session to end"); + synchronized (session) { + waitWithTimeout(() -> session[0] != null, session::wait); + } + } + + @Test + @LargeTest + public void abortSelectingPrinter() throws Exception { + final StubbablePrinterDiscoverySession session[] = new StubbablePrinterDiscoverySession[1]; + final PrintAttributes printAttributes[] = new PrintAttributes[1]; + + setMockPrintServiceCallbacks(session); + print(printAttributes); + + // We are now in the PrintActivity + Log.i(LOG_TAG, "Waiting for session"); + synchronized (session) { + waitWithTimeout(() -> session[0] == null, session::wait); + } + + setPrinter("1st printer"); + + Log.i(LOG_TAG, "Waiting for print attributes to change"); + synchronized (printAttributes) { + waitWithTimeout( + () -> printAttributes[0] == null || !printAttributes[0].getMediaSize().equals( + PrintAttributes.MediaSize.ISO_A0), printAttributes::wait); + } + + setPrinter("All printers\u2026"); + + // We are now in the SelectPrinterActivity + clickOnText("Add printer"); + + // We are now in the AddPrinterActivity + AddPrintersActivity.addObserver( + () -> addPrinter(session[0], "2nd printer", PrintAttributes.MediaSize.ISO_A1)); + + // This executes the observer registered above + clickOn(new UiSelector().text(MockPrintService.class.getCanonicalName()) + .resourceId("com.android.printspooler:id/title")); + + getUiDevice().pressBack(); + AddPrintersActivity.clearObservers(); + + // Do not select a new printer, just press back + getUiDevice().pressBack(); + + // We are now in the PrintActivity + // The media size should not change + Log.i(LOG_TAG, "Make sure print attributes did not change"); + Thread.sleep(100); + assertEquals(PrintAttributes.MediaSize.ISO_A0, printAttributes[0].getMediaSize()); + + getUiDevice().pressBack(); + + // We are back in the test activity + Log.i(LOG_TAG, "Waiting for session to end"); + synchronized (session) { + waitWithTimeout(() -> session[0] != null, session::wait); + } + } +} diff --git a/core/tests/coretests/src/android/print/mockservice/AddPrintersActivity.java b/core/tests/coretests/src/android/print/mockservice/AddPrintersActivity.java new file mode 100644 index 000000000000..8f1a9edeacba --- /dev/null +++ b/core/tests/coretests/src/android/print/mockservice/AddPrintersActivity.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.print.mockservice; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.NonNull; + +import java.util.ArrayList; + +public class AddPrintersActivity extends Activity { + private static final ArrayList<Runnable> sObservers = new ArrayList<>(); + + public static void addObserver(@NonNull Runnable observer) { + synchronized (sObservers) { + sObservers.add(observer); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + synchronized (sObservers) { + for (Runnable sObserver : sObservers) { + sObserver.run(); + } + } + + finish(); + } + + public static void clearObservers() { + synchronized (sObservers) { + sObservers.clear(); + } + } +} diff --git a/core/tests/coretests/src/android/print/mockservice/StubbablePrinterDiscoverySession.java b/core/tests/coretests/src/android/print/mockservice/StubbablePrinterDiscoverySession.java index e132d79cfd8b..f3a5373722cc 100644 --- a/core/tests/coretests/src/android/print/mockservice/StubbablePrinterDiscoverySession.java +++ b/core/tests/coretests/src/android/print/mockservice/StubbablePrinterDiscoverySession.java @@ -16,6 +16,7 @@ package android.print.mockservice; +import android.support.annotation.NonNull; import android.os.CancellationSignal; import android.print.PrinterId; import android.printservice.CustomPrinterIconCallback; @@ -42,7 +43,7 @@ public class StubbablePrinterDiscoverySession extends PrinterDiscoverySession { } @Override - public void onStartPrinterDiscovery(List<PrinterId> priorityList) { + public void onStartPrinterDiscovery(@NonNull List<PrinterId> priorityList) { if (mCallbacks != null) { mCallbacks.onStartPrinterDiscovery(priorityList); } @@ -56,29 +57,30 @@ public class StubbablePrinterDiscoverySession extends PrinterDiscoverySession { } @Override - public void onValidatePrinters(List<PrinterId> printerIds) { + public void onValidatePrinters(@NonNull List<PrinterId> printerIds) { if (mCallbacks != null) { mCallbacks.onValidatePrinters(printerIds); } } @Override - public void onStartPrinterStateTracking(PrinterId printerId) { + public void onStartPrinterStateTracking(@NonNull PrinterId printerId) { if (mCallbacks != null) { mCallbacks.onStartPrinterStateTracking(printerId); } } @Override - public void onRequestCustomPrinterIcon(PrinterId printerId, - CancellationSignal cancellationSignal, CustomPrinterIconCallback callback) { + public void onRequestCustomPrinterIcon(@NonNull PrinterId printerId, + @NonNull CancellationSignal cancellationSignal, + @NonNull CustomPrinterIconCallback callback) { if (mCallbacks != null) { mCallbacks.onRequestCustomPrinterIcon(printerId, cancellationSignal, callback); } } @Override - public void onStopPrinterStateTracking(PrinterId printerId) { + public void onStopPrinterStateTracking(@NonNull PrinterId printerId) { if (mCallbacks != null) { mCallbacks.onStopPrinterStateTracking(printerId); } diff --git a/docs/html/_redirects.yaml b/docs/html/_redirects.yaml index 8ddb98239038..8cb2ee7b20ad 100644 --- a/docs/html/_redirects.yaml +++ b/docs/html/_redirects.yaml @@ -430,7 +430,13 @@ redirects: - from: /training/cloudsync/aesync.html to: /google/gcm/index.html - from: /training/cloudsync/index.html - to: /training/backup/index.html + to: /guide/topics/data/backup.html +- from: /training/backup/index.html + to: /guide/topics/data/backup.html +- from: /training/backup/autosyncapi.html + to: /guide/topics/data/autobackup.html +- from: /training/backup/backupapi.html + to: /guide/topics/data/keyvaluebackup.html - from: /training/basics/location/... to: /training/location/... - from: /training/monetization/index.html @@ -796,7 +802,7 @@ redirects: - from: /preview/features/app-linking.html to: /training/app-links/index.html - from: /preview/backup/index.html - to: /training/backup/autosyncapi.html + to: /guide/topics/data/backup/autobackup.html - from: /preview/features/power-mgmt.html to: /training/monitoring-device-state/doze-standby.html - from: /preview/dev-community @@ -842,6 +848,10 @@ redirects: to: /topic/performance/power/network/gather-data.html - from: /training/performance/battery/network/index.html to: /topic/performance/power/network/index.html +- from: /training/articles/memory.html + to: /topic/performance/memory.html +- from: /topic/performance/optimizing-view-hierarchies.html + to: /topic/performance/rendering/optimizing-view-hierarchies.html # Redirects for the new [dac]/topic/libraries/ area @@ -1242,6 +1252,44 @@ redirects: to: /studio/write/lint.html?utm_source=android-studio - from: /r/studio-ui/gradle-console.html to: /studio/run/index.html?utm_source=android-studio#gradle-console +- from: /r/studio-ui/app-indexing-test.html + to: /studio/write/app-link-indexing.html#appindexingtest?utm_source=android-studio +- from: /r/studio-ui/vcs.html + to: /studio/intro/index.html#version_control_basics?utm_source=android-studio +- from: /r/studio-ui/create-new-module.html + to: /studio/projects/index.html#ApplicationModules?utm_source=android-studio +- from: /r/studio-ui/build-variants.html + to: /studio/run/index.html#changing-variant?utm_source=android-studio +- from: /r/studio-ui/generate-signed-apk.html + to: /studio/publish/app-signing.html#release-mode?utm_source=android-studio +- from: /r/studio-ui/import-project-vcs.html + to: /studio/projects/create-project.html#ImportAProject?utm_source=android-studio +- from: /r/studio-ui/apk-analyzer.html + to: /studio/build/apk-analyzer.html?utm_source=android-studio +- from: /r/studio-ui/breakpoints.html + to: /studio/debug/index.html#breakPointsView?utm_source=android-studio +- from: /r/studio-ui/attach-debugger-to-process.html + to: /studio/debug/index.html?utm_source=android-studio +- from: /r/studio-ui/import-sample.html + to: /samples/index.html?utm_source=android-studio +- from: /r/studio-ui/import-module.html + to: /studio/projects/add-app-module.html#ImportAModule?utm_source=android-studio +- from: /r/studio-ui/import-project.html + to: /studio/projects/create-project.html#ImportAProject?utm_source=android-studio +- from: /r/studio-ui/create-project.html + to: /studio/projects/create-project.html?utm_source=android-studio +- from: /r/studio-ui/new-activity.html + to: /studio/projects/template.html?utm_source=android-studio +- from: /r/studio-ui/new-resource-file.html + to: /studio/write/add-resources.html?utm_source=android-studio +- from: /r/studio-ui/new-resource-dir.html + to: /studio/write/add-resources.html#add_a_resource_directory?utm_source=android-studio +- from: /r/studio-ui/configure-component.html + to: /studio/write/add-resources.html?utm_source=android-studio +- from: /r/studio-ui/ninepatch.html + to: /studio/write/draw9patch.html?utm_source=android-studio +- from: /r/studio-ui/firebase-assistant.html + to: /studio/write/firebase.html?utm_source=android-studio # Redirects from (removed) N Preview documentation - from: /preview/features/afw.html diff --git a/docs/html/guide/_book.yaml b/docs/html/guide/_book.yaml index 13c948cc297c..f09fe771bd1a 100644 --- a/docs/html/guide/_book.yaml +++ b/docs/html/guide/_book.yaml @@ -396,6 +396,13 @@ toc: path: /guide/topics/data/data-storage.html - title: Data Backup path: /guide/topics/data/backup.html + section: + - title: Auto Backup + path: /guide/topics/data/autobackup.html + - title: Key/Value Backup + path: /guide/topics/data/keyvaluebackup.html + - title: Testing Backup and Restore + path: /guide/topics/data/testingbackup.html - title: App Install Location path: /guide/topics/data/install-location.html diff --git a/docs/html/guide/guide_toc.cs b/docs/html/guide/guide_toc.cs index 9257a76e93f2..8fe3f201fc2e 100644 --- a/docs/html/guide/guide_toc.cs +++ b/docs/html/guide/guide_toc.cs @@ -545,9 +545,16 @@ <li><a href="<?cs var:toroot ?>guide/topics/data/data-storage.html"> <span class="en">Storage Options</span> </a></li> - <li><a href="<?cs var:toroot ?>guide/topics/data/backup.html"> + <li class="nav-section"> + <div class="nav-section-header"><a href="<?cs var:toroot ?>guide/topics/data/backup.html"> <span class="en">Data Backup</span> - </a></li> + </a></div> + <ul> + <li><a href="<?cs var:toroot ?>guide/topics/data/autobackup.html">Auto Backup</a></li> + <li><a href="<?cs var:toroot ?>guide/topics/data/keyvaluebackup.html">Key/Value Backup</a></li> + <li><a href="<?cs var:toroot ?>guide/topics/data/testingbackup.html">Testing Backup and Restore</a></li> + </ul> + </li> <li><a href="<?cs var:toroot ?>guide/topics/data/install-location.html"> <span class="en">App Install Location</span> </a></li> diff --git a/docs/html/guide/topics/data/autobackup.jd b/docs/html/guide/topics/data/autobackup.jd new file mode 100644 index 000000000000..3be09d712884 --- /dev/null +++ b/docs/html/guide/topics/data/autobackup.jd @@ -0,0 +1,257 @@ +page.title=Auto Backup for Apps +page.tags=backup, marshmallow, androidm +page.keywords=backup, autobackup +page.image=images/cards/card-auto-backup_2x.png + +@jd:body + +<div id="qv-wrapper"> +<div id="qv"> + <h2>In this document</h2> + <ol> + <li><a href="#Files">Files that are backed up</a></li> + <li><a href="#BackupLocation">Backup location</a></li> + <li><a href="#BackupSchedule">Backup schedule</a></li> + <li><a href="#RestoreSchedule">Restore schedule</a></li> + <li><a href="#EnablingAutoBackup">Enabling and disabling Auto Backup</a></li> + <li><a href="#IncludingFiles">Including and excluding files</a><ul> + <li><a href="#XMLSyntax">XML config syntax</a></li> + </ul></li> + <li><a href="#ImplementingBackupAgent">Implementing BackupAgent</a></li> + </ol> + + <h2>Key classes</h2> + <ol> + <li>{@link android.app.backup.BackupAgent}</li> + <li><a href="{@docRoot}reference/android/R.attr.html">R.attr</a></li> + </ol> + +</div> +</div> + +Since Android 6.0 (API 23), Android has offered the <em>Auto Backup for Apps</em> feature as +a way for developers to quickly add backup functionality to their apps. Auto +Backup preserves app data by uploading it to the user’s Google Drive account. +The amount of data is limited to 25MB per user of your app and there is no +charge for storing backup data. + +<h2 id="Files">Files that are backed up</h2> +<p>By default, Auto Backup includes files in most of the directories that are +assigned to your app by the system: +<ul> + <li>Shared preferences files. + <li>Files in the directory returned by {@link android.content.Context#getFilesDir()}. + <li>Files in the directory returned by {@link android.content.Context#getDatabasePath(String)}, + which also includes files created with the + {@link android.database.sqlite.SQLiteOpenHelper} class. + <li>Files in directories created with {@link android.content.Context#getDir(String,int)}. + <li>Files on external storage in the directory returned by + {@link android.content.Context#getExternalFilesDir(String)}.</li></ul> + +<p>Auto Backup excludes files in directories returned by + {@link android.content.Context#getCacheDir()}, + {@link android.content.Context#getCodeCacheDir()}, or + {@link android.content.Context#getNoBackupFilesDir()}. The files saved + in these locations are only needed temporarily, or are intentionally + excluded from backup operations. + +<p>You can configure your app to include and exclude particular files. +For more information, see the <a href="#IncludingFiles">Include and exclude files</a> +section. + +<h2 id="BackupLocation">Backup location</h2> +<p>Backup data is stored in a private folder in the user's Google Drive account, +limited to 25MB per app. The saved data does not count towards the user's +personal Google Drive quota. Only the most recent backup is stored. When a +backup is made, the previous backup (if one exists) is deleted. + +<p>Users can see a list of apps that have been backed up in the Google Drive app under +<strong>Settings -> Auto Backup for apps -> Manage backup</strong>. The +backup data cannot be read by the user or other applications on the device. + +<p>Backups from each device-setup-lifetime are stored in separate datasets +as shown in the following examples: +<ul> + <li>If the user owns two devices, then a backup dataset exists for each device. + <li>If the user factory resets a device and then sets up the device with the + same account, the backup is stored in a new dataset. Obsolete datasets are + automatically deleted after a period of inactivity.</li></ul> + +<p class="caution"><strong>Caution:</strong> Once the amount of data reaches +25MB, the app is banned from sending data to the +cloud, even if the amount of data later falls under the 25MB threshold. The ban +affects only the offending device (not other devices that the user owns) and +lasts for the entire device-setup-lifetime. For example, if the user removes and +reinstalls the application, the ban is still in effect. The ban is lifted when +the user performs factory reset on the device. + +<h2 id="BackupSchedule">Backup schedule</h2> +<p>Backups occur automatically when all of the following conditions are met: +<ul> + <li>The user has enabled backup on the device in <strong>Settings</strong> > + <strong>Backup & Reset</strong>. + <li>At least 24 hours have elapsed since the last backup. + <li>The device is idle and charging. + <li>The device is connected to a Wi-Fi network. If the device is never connected + to a wifi network, then Auto Backup never occurs.</li></ul> + +<p>In practice, these conditions occur roughly every night. To conserve network +bandwidth, upload takes place only if app data has changed. + +<p>During Auto Backup, the system shuts down the app to make sure it is no longer +writing to the file system. By default, the backup system ignores apps that are +running in the foreground because users would notice their apps being shut down. +You can override the default behavior by setting the +<a href="{@docRoot}reference/android/R.attr.html#backupInForeground">backupInForeground</a> +attribute to true. + +<p>To simplify testing, Android includes tools that let you manually initiate +a backup of your app. For more information, see +<a href="{@docRoot}guide/topics/data/testingbackup.html">Testing Backup and Restore</a>. + +<h2 id="RestoreSchedule">Restore schedule</h2> +<p>Data is restored whenever the app is installed, either from the Play store, +during device setup (when the system installs previously installed apps), or +from running adb install. The restore operation occurs after the APK is +installed, but before the app is available to be launched by the user. + +<p>During the initial device setup wizard, the user is shown a list of available backup +datasets and is asked which one to restore the data from. Whichever backup +dataset is selected becomes the ancestral dataset for the device. The device can +restore from either its own backups or the ancestral dataset. The device +prioritize its own backup if backups from both sources are available. If the +user didn't go through the device setup wizard, then the device can restore only from +its own backups. + +<p>To simplify testing, Android includes tools that let you manually initiate +a restore of your app. For more information, see +<a href="{@docRoot}guide/topics/data/testingbackup.html">Testing Backup and Restore</a>. + +<h2 id="EnablingAutoBackup">Enabling and disabling backup</h2> +<p>Apps that target Android 6.0 (API level 23) or higher automatically participate +in Auto Backup. This is because the +<a href="{@docRoot}reference/android/R.attr.html#allowBackup">android:allowBackup</a> +attribute, which enables/disables backup, defaults to <code>true</code> if omitted. +To avoid confusion, we recommend you explicitly set the attribute in the <code><application></code> +element of your <code>AndroidManifest.xml</code>. For example: + +<pre class="prettyprint"><application ... + android:allowBackup="true"> +</app></pre> + +<p>To disable Auto Backup, add either of the following attributes to the +application element in your manifest file: + +<ul> + <li>set <code>android:allowBackup</code> to <code>false</code>. This completely disables data + backup. You may want to disable backups when your app can recreate its state + through some other mechanism or when your app deals with sensitive + information that should not be backed up.</li> + <li>set <code>android:allowBackup</code> to <code>true</code> and + <code>android:fullBackupOnly</code> to <code>false</code>. With these settings, + your app always participates in Key/Value Backup, even when running on devices that + support Auto Backup.</li></ul> + +<h2 id="IncludingFiles">Including and excluding files</h2> +<p>By default, the system backs up almost all app data. For more information, +see <a href="#Files">Files that are backed up</a>. This section shows you how to +define custom XML rules to control what gets backed up. + +<ol> + <li>In <code>AndroidManifest.xml</code>, add the <a href="{@docRoot}reference/android/R.attr.html#fullBackupContent">android:fullBackupContent</a> attribute to the + <code><application></code> element. This attribute points to an XML file that contains backup + rules. For example: + <pre class="prettyprint"><application ... + android:fullBackupContent="@xml/my_backup_rules"> + </app></pre></li> + <li>Create an XML file called <code>my_backup_rules.xml</code> in the <code>res/xml/</code> directory. Inside the file, add rules with the <code><include></code> and <code><exclude></code> elements. The following sample backs up all shared preferences except <code>device.xml</code>: + <pre><?xml version="1.0" encoding="utf-8"?> +<full-backup-content> + <include domain="sharedpref" path="."/> + <exclude domain="sharedpref" path="device.xml"/> +</full-backup-content></pre></li> + +<h3 id="XMLSyntax">XML Config Syntax</h3> +<p>The XML syntax for the configuration file is shown below: + +<pre class="prettyprint"><full-backup-content> + <include domain=["file" | "database" | "sharedpref" | "external" | "root"] + path="string" /> + <exclude domain=["file" | "database" | "sharedpref" | "external" | "root"] + path="string" /> +</full-backup-content></pre> + +<p>Inside the <code><full-backup-content></code> tag, you can define <code><include></code> and <code><exclude></code> +elements: + +<ul> + <li><code><include></code> - Specifies a file or folder to backup. By default, Auto Backup +includes almost all app files. If you specify an <include> element, the system +no longer includes any files by default and backs up <em>only the files +specified</em>. To include multiple files, use multiple <include> elements. + <p>note: Files in directories returned by <code>getCacheDir()</code>, <code>getCodeCacheDir()</code>, or +<code>getNoBackupFilesDir()</code> are always excluded even if you try to include them.</li> + + <li><code><exclude></code> - Specifies a file or folder to exclude during backup. Here are +some files that are typically excluded from backup: <ul> + <li>Files that have device specific identifiers, either issued by a server or +generated on the device. For example, <a href="https://developers.google.com/cloud-messaging/android/start">Google Cloud Messaging (GCM)</a> needs to +generate a registration token every time a user installs your app on a new +device. If the old registration token is restored, the app may behave +unexpectedly. + <li>Account credentials or other sensitive information. Consider asking the +user to reauthenticate the first time they launch a restored app rather than +allowing for storage of such information in the backup. + <li>Files related to app debugging, such as <a href="{@docRoot}studio/run/index.html#instant-run">instant run files</a>. To exclude instant run files, add the rule <code><exclude +domain="file" path="instant-run"/></code> + <li>Large files that cause the app to exceed the 25MB backup quota.</li> </ul> + </li> </ul> + +<p class="note"><strong>Note:</strong> If your configuration file specifies both elements, then the +backup contains everything captured by the <code><include></code> elements minus the +resources named in the <code><exclude></code> elements. In other words, +<code><exclude></code> takes precedence. + +<p>Each element must include the following two attributes: +<ul> + <li><code>domain</code> - specifies the location of resource. Valid values for this attribute +include the following: <ul> + <li><code>root</code> - the directory on the filesystem where all private files belonging to +this app are stored. + <li><code>file</code> - directories returned by {@link android.content.Context#getFilesDir()}. + <li><code>database</code> - directories returned by {@link android.content.Context#getDatabasePath(String) getDatabasePath()}. +Databases created with {@link android.database.sqlite.SQLiteOpenHelper} +are stored here. + <li><code>sharedpref</code> - the directory where {@link android.content.SharedPreferences} +are stored. + <li><code>external</code> the directory returned by {@link android.content.Context#getExternalFilesDir(String) getExternalFilesDir()} + <p>Note: You cannot backup files outside of these locations.</li></ul> + <li><code>path</code>: Specifies a file or folder to include in or exclude from backup. Note +that: <ul> + <li>This attribute does not support wildcard or regex syntax. + <li>You can use <code>.</code> to reference the current directory, however, you cannot +reference the parent directory <code>..</code> for security reasons. + <li>If you specify a directory, then the rule applies to all files in the +directory and recursive sub-directories.</li></ul></li></ul> + +<h2 id="ImplementingBackupAgent">Implementing BackupAgent</h2> +<p>Apps that implement Auto Backup do not need to implement {@link android.app.backup.BackupAgent}. However, you can optionally implement a custom {@link android.app.backup.BackupAgent}. Typically, there are two reasons for doing this: +<ul> + <li>You want to receive notification of backup events such as, +{@link android.app.backup.BackupAgent#onRestoreFinished()} or {@link android.app.backup.BackupAgent#onQuotaExceeded(long,long)}. These callback methods are executed +even if the app is not running. +<li>You can't easily express the set of files you want to backup with XML rules. +In these very rare cases, you can implement a BackupAgent that overrides {@link android.app.backup.BackupAgent#onFullBackup(FullBackupDataOutput)} to +store what you want. To retain the system's default implementation, call the +corresponding method on the superclass with <code>super.onFullBackup()</code>.</li></ul> + +<p class="note"><strong>Note:</strong> Your <code>BackupAgent</code> must +implement the abstract methods +{@link android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) onBackup()} +and {@link android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) onRestore()}. +Those methods are used for Key/Value Backup. So if +you are not using Key/Value Backup, implement those methods and leave them blank. + +<p>For more information, see +<a href="{@docRoot}guide/topics/data/keyvaluebackup.html#BackupAgent">Extending +BackupAgent</a>.
\ No newline at end of file diff --git a/docs/html/guide/topics/data/backup.jd b/docs/html/guide/topics/data/backup.jd index 619c79021870..a688c6e9889b 100644 --- a/docs/html/guide/topics/data/backup.jd +++ b/docs/html/guide/topics/data/backup.jd @@ -1,930 +1,34 @@ -page.title=Data Backup -@jd:body - - -<div id="qv-wrapper"> -<div id="qv"> - - <h2>Quickview</h2> - <ul> - <li>Back up the user's data to the cloud in case the user loses it</li> - <li>If the user upgrades to a new Android-powered device, your app can restore the user's -data onto the new device</li> - <li>Easily back up SharedPreferences and private files with BackupAgentHelper</li> - <li>Requires API Level 8</li> - </ul> - - <h2>In this document</h2> - <ol> - <li><a href="#Basics">The Basics</a></li> - <li><a href="#BackupManifest">Declaring the Backup Agent in Your Manifest</a></li> - <li><a href="#BackupKey">Registering for Android Backup Service</a></li> - <li><a href="#BackupAgent">Extending BackupAgent</a> - <ol> - <li><a href="#RequiredMethods">Required Methods</a></li> - <li><a href="#PerformingBackup">Performing backup</a></li> - <li><a href="#PerformingRestore">Performing restore</a></li> - </ol> - </li> - <li><a href="#BackupAgentHelper">Extending BackupAgentHelper</a> - <ol> - <li><a href="#SharedPreferences">Backing up SharedPreferences</a></li> - <li><a href="#Files">Backing up Private Files</a></li> - </ol> - </li> - <li><a href="#RestoreVersion">Checking the Restore Data Version</a></li> - <li><a href="#RequestingBackup">Requesting Backup</a></li> - <li><a href="#RequestingRestore">Requesting Restore</a></li> - <li><a href="#Testing">Testing Your Backup Agent</a></li> - </ol> - - <h2>Key classes</h2> - <ol> - <li>{@link android.app.backup.BackupManager}</li> - <li>{@link android.app.backup.BackupAgent}</li> - <li>{@link android.app.backup.BackupAgentHelper}</li> - </ol> - - <h2>See also</h2> - <ol> - <li><a href="{@docRoot}tools/help/bmgr.html">{@code bmgr} tool</a></li> - </ol> - -</div> -</div> - -<p>Android's {@link android.app.backup backup} service allows you to copy your persistent -application data to remote "cloud" storage, in order to provide a restore point for the -application data and settings. If a user performs a factory reset or converts to a new -Android-powered device, the system automatically restores your backup data when the application -is re-installed. This way, your users don't need to reproduce their previous data or -application settings. This process is completely transparent to the user and does not affect the -functionality or user experience in your application.</p> - -<p>During a backup operation (which your application can request), Android's Backup Manager ({@link -android.app.backup.BackupManager}) queries your application for backup data, then hands it to -a backup transport, which then delivers the data to the cloud storage. During a -restore operation, the Backup Manager retrieves the backup data from the backup transport and -returns it to your application so your application can restore the data to the device. It's -possible for your application to request a restore, but that shouldn't be necessary—Android -automatically performs a restore operation when your application is installed and there exists -backup data associated with the user. The primary scenario in which backup data is restored is when -a user resets their device or upgrades to a new device and their previously installed -applications are re-installed.</p> - -<p class="note"><strong>Note:</strong> The backup service is <em>not</em> designed for -synchronizing application data with other clients or saving data that you'd like to access during -the normal application lifecycle. You cannot read or write backup data on demand and cannot access -it in any way other than through the APIs provided by the Backup Manager.</p> - -<p>The backup transport is the client-side component of Android's backup framework, which is -customizable by -the device manufacturer and service provider. The backup transport may differ from device to device -and which backup transport is available on any given device is transparent to your application. The -Backup Manager APIs isolate your application from the actual backup transport available on a given -device—your application communicates with the Backup Manager through a fixed set of APIs, -regardless of the underlying transport.</p> - -<p>Data backup is <em>not</em> guaranteed to be available on all Android-powered -devices. However, your application is not adversely affected in the event -that a device does not provide a backup transport. If you believe that users will benefit from data -backup in your application, then you can implement it as described in this document, test it, then -publish your application without any concern about which devices actually perform backup. When your -application runs on a device that does not provide a backup transport, your application operates -normally, but will not receive callbacks from the Backup Manager to backup data.</p> - -<p>Although you cannot know what the current transport is, you are always assured that your -backup data cannot be read by other applications on the device. Only the Backup Manager and backup -transport have access to the data you provide during a backup operation.</p> - -<p class="caution"><strong>Caution:</strong> Because the cloud storage and transport service can -differ from device to device, Android makes no guarantees about the security of your data while -using backup. You should always be cautious about using backup to store sensitive data, such as -usernames and passwords.</p> - - -<h2 id="Basics">The Basics</h2> - -<p>To backup your application data, you need to implement a backup agent. Your backup -agent is called by the Backup Manager to provide the data you want to back up. It is also called -to restore your backup data when the application is re-installed. The Backup Manager handles all -your data transactions with the cloud storage (using the backup transport) and your backup agent -handles all your data transactions on the device.</p> - -<p>To implement a backup agent, you must:</p> - -<ol> - <li>Declare your backup agent in your manifest file with the <a -href="{@docRoot}guide/topics/manifest/application-element.html#agent">{@code -android:backupAgent}</a> attribute.</li> - <li>Register your application with a backup service. Google offers <a -href="http://code.google.com/android/backup/index.html">Android Backup Service</a> as a backup -service for most Android-powered devices, which requires that you register your application in -order for it to work. Any other backup services available might also require you to register -in order to store your data on their servers.</li> - <li>Define a backup agent by either:</p> - <ol type="a"> - <li><a href="#BackupAgent">Extending BackupAgent</a> - <p>The {@link android.app.backup.BackupAgent} class provides the central interface with -which your application communicates with the Backup Manager. If you extend this class -directly, you must override {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} and {@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) -onRestore()} to handle the backup and restore operations for your data.</p> - <p><em>Or</em></p> - <li><a href="#BackupAgentHelper">Extending BackupAgentHelper</a> - <p>The {@link android.app.backup.BackupAgentHelper} class provides a convenient -wrapper around the {@link android.app.backup.BackupAgent} class, which minimizes the amount of code -you need to write. In your {@link android.app.backup.BackupAgentHelper}, you must use one or more -"helper" objects, which automatically backup and restore certain types of data, so that you do not -need to implement {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} and {@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) -onRestore()}.</p> - <p>Android currently provides backup helpers that will backup and restore complete files -from {@link android.content.SharedPreferences} and <a -href="{@docRoot}guide/topics/data/data-storage.html#filesInternal">internal storage</a>.</p> - </li> - </ol> - </li> -</ol> - - - -<h2 id="BackupManifest">Declaring the Backup Agent in Your Manifest</h2> - -<p>This is the easiest step, so once you've decided on the class name for your backup agent, declare -it in your manifest with the <a -href="{@docRoot}guide/topics/manifest/application-element.html#agent">{@code -android:backupAgent}</a> attribute in the <a -href="{@docRoot}guide/topics/manifest/application-element.html">{@code -<application>}</a> tag.</p> - -<p>For example:</p> - -<pre> -<manifest ... > - ... - <application android:label="MyApplication" - <b>android:backupAgent="MyBackupAgent"</b>> - <activity ... > - ... - </activity> - </application> -</manifest> -</pre> - -<p>Another attribute you might want to use is <a -href="{@docRoot}guide/topics/manifest/application-element.html#restoreany">{@code -android:restoreAnyVersion}</a>. This attribute takes a boolean value to indicate whether you -want to restore the application data regardless of the current application version compared to the -version that produced the backup data. (The default value is "{@code false}".) See <a -href="#RestoreVersion">Checking the Restore Data Version</a> for more information.</p> - -<p class="note"><strong>Note:</strong> The backup service and the APIs you must use are -available only on devices running API Level 8 (Android 2.2) or greater, so you should also -set your <a -href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#min">{@code android:minSdkVersion}</a> -attribute to "8".</p> - - - - -<h2 id="BackupKey">Registering for Android Backup Service</h2> - -<p>Google provides a backup transport with <a -href="http://code.google.com/android/backup/index.html">Android Backup Service</a> for most -Android-powered devices running Android 2.2 or greater.</p> - -<p>In order for your application to perform backup using Android Backup Service, you must -register your application with the service to receive a Backup Service Key, then -declare the Backup Service Key in your Android manifest.</p> - -<p>To get your Backup Service Key, <a -href="http://code.google.com/android/backup/signup.html">register for Android Backup Service</a>. -When you register, you will be provided a Backup Service Key and the appropriate {@code -<meta-data>} XML code for your Android manifest file, which you must include as a child of the -{@code <application>} element. For example:</p> - -<pre> -<application android:label="MyApplication" - android:backupAgent="MyBackupAgent"> - ... - <meta-data android:name="com.google.android.backup.api_key" - android:value="AEdPqrEAAAAIDaYEVgU6DJnyJdBmU7KLH3kszDXLv_4DIsEIyQ" /> -</application> -</pre> - -<p>The <code>android:name</code> must be <code>"com.google.android.backup.api_key"</code> and -the <code>android:value</code> must be the Backup Service Key received from the Android Backup -Service registration.</p> - -<p>If you have multiple applications, you must register each one, using the respective package -name.</p> - -<p class="note"><strong>Note:</strong> The backup transport provided by Android Backup Service is -not guaranteed to be available -on all Android-powered devices that support backup. Some devices might support backup -using a different transport, some devices might not support backup at all, and there is no way for -your application to know what transport is used on the device. However, if you implement backup for -your application, you should always include a Backup Service Key for Android Backup Service so -your application can perform backup when the device uses the Android Backup Service transport. If -the device does not use Android Backup Service, then the {@code <meta-data>} element with the -Backup Service Key is ignored.</p> - - - - -<h2 id="BackupAgent">Extending BackupAgent</h2> - -<p>Most applications shouldn't need to extend the {@link android.app.backup.BackupAgent} class -directly, but should instead <a href="#BackupAgentHelper">extend BackupAgentHelper</a> to take -advantage of the built-in helper classes that automatically backup and restore your files. However, -you might want to extend {@link android.app.backup.BackupAgent} directly if you need to:</p> -<ul> - <li>Version your data format. For instance, if you anticipate the need to revise the -format in which you write your application data, you can build a backup agent to cross-check your -application version during a restore operation and perform any necessary compatibility work if the -version on the device is different than that of the backup data. For more information, see <a -href="#RestoreVersion">Checking the Restore Data Version</a>.</li> - <li>Instead of backing up an entire file, you can specify the portions of data the should be -backed up and how each portion is then restored to the device. (This can also help you manage -different versions, because you read and write your data as unique entities, rather than -complete files.)</li> - <li>Back up data in a database. If you have an SQLite database that you want to restore when -the user re-installs your application, you need to build a custom {@link -android.app.backup.BackupAgent} that reads the appropriate data during a backup operation, then -create your table and insert the data during a restore operation.</li> -</ul> - -<p>If you don't need to perform any of the tasks above and want to back up complete files from -{@link android.content.SharedPreferences} or <a -href="{@docRoot}guide/topics/data/data-storage.html#filesInternal">internal storage</a>, you -should skip to <a href="#BackupAgentHelper">Extending BackupAgentHelper</a>.</p> - - - -<h3 id="RequiredMethods">Required Methods</h3> - -<p>When you create a backup agent by extending {@link android.app.backup.BackupAgent}, you -must implement the following callback methods:</p> - -<dl> - <dt>{@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()}</dt> - <dd>The Backup Manager calls this method after you <a href="#RequestingBackup">request a -backup</a>. In this method, you read your application data from the device and pass the data you -want to back up to the Backup Manager, as described below in <a href="#PerformingBackup">Performing -backup</a>.</dd> - - <dt>{@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) -onRestore()}</dt> - <dd>The Backup Manager calls this method during a restore operation (you can <a -href="#RequestingRestore">request a restore</a>, but the system automatically performs restore when -the user re-installs your application). When it calls this method, the Backup Manager delivers your -backup data, which you then restore to the device, as described below in <a -href="#PerformingRestore">Performing restore</a>.</dd> -</dl> - - - -<h3 id="PerformingBackup">Performing backup</h3> - - -<p>When it's time to back up your application data, the Backup Manager calls your {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} method. This is where you must provide your application data to the Backup Manager so -it can be saved to cloud storage.</p> - -<p>Only the Backup Manager can call your backup agent's {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} method. Each time that your application data changes and you want to perform a backup, -you must request a backup operation by calling {@link -android.app.backup.BackupManager#dataChanged()} (see <a href="#RequestingBackup">Requesting -Backup</a> for more information). A backup request does not result in an immediate call to your -{@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} method. Instead, the Backup Manager waits for an appropriate time, then performs -backup for all applications that have requested a backup since the last backup was performed.</p> - -<p class="note"><strong>Tip:</strong> While developing your application, you can initiate an -immediate backup operation from the Backup Manager with the <a -href="{@docRoot}tools/help/bmgr.html">{@code bmgr} tool</a>.</p> - -<p>When the Backup Manager calls your {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} method, it passes three parameters:</p> - -<dl> - <dt>{@code oldState}</dt> - <dd>An open, read-only {@link android.os.ParcelFileDescriptor} pointing to the last backup -state provided by your application. This is not the backup data from cloud storage, but a -local representation of the data that was backed up the last time {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} was called (as defined by {@code newState}, below, or from {@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) -onRestore()}—more about this in the next section). Because {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} does not allow you to read existing backup data in -the cloud storage, you can use this local representation to determine whether your data has changed -since the last backup.</dd> - <dt>{@code data}</dt> - <dd>A {@link android.app.backup.BackupDataOutput} object, which you use to deliver your backup -data to the Backup Manager.</dd> - <dt>{@code newState}</dt> - <dd>An open, read/write {@link android.os.ParcelFileDescriptor} pointing to a file in which -you must write a representation of the data that you delivered to {@code data} (a representation -can be as simple as the last-modified timestamp for your file). This object is -returned as {@code oldState} the next time the Backup Manager calls your {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} method. If you do not write your backup data to {@code newState}, then {@code oldState} -will point to an empty file next time Backup Manager calls {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()}.</dd> -</dl> - -<p>Using these parameters, you should implement your {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} method to do the following:</p> - -<ol> - <li>Check whether your data has changed since the last backup by comparing {@code oldState} to -your current data. How you read data in {@code oldState} depends on how you originally wrote it to -{@code newState} (see step 3). The easiest way to record the state of a file is with its -last-modified timestamp. For example, here's how you can read and compare a timestamp from {@code -oldState}: - <pre> -// Get the oldState input stream -FileInputStream instream = new FileInputStream(oldState.getFileDescriptor()); -DataInputStream in = new DataInputStream(instream); - -try { - // Get the last modified timestamp from the state file and data file - long stateModified = in.readLong(); - long fileModified = mDataFile.lastModified(); - - if (stateModified != fileModified) { - // The file has been modified, so do a backup - // Or the time on the device changed, so be safe and do a backup - } else { - // Don't back up because the file hasn't changed - return; - } -} catch (IOException e) { - // Unable to read state file... be safe and do a backup -} -</pre> - <p>If nothing has changed and you don't need to back up, skip to step 3.</p> - </li> - <li>If your data has changed, compared to {@code oldState}, write the current data to -{@code data} to back it up to the cloud storage. - <p>You must write each chunk of data as an "entity" in the {@link -android.app.backup.BackupDataOutput}. An entity is a flattened binary data -record that is identified by a unique key string. Thus, the data set that you back up is -conceptually a set of key-value pairs.</p> - <p>To add an entity to your backup data set, you must:</p> - <ol> - <li>Call {@link android.app.backup.BackupDataOutput#writeEntityHeader(String,int) -writeEntityHeader()}, passing a unique string key for the data you're about to write and the data -size.</li> - <li>Call {@link android.app.backup.BackupDataOutput#writeEntityData(byte[],int) -writeEntityData()}, passing a byte buffer that contains your data and the number of bytes to write -from the buffer (which should match the size passed to {@link -android.app.backup.BackupDataOutput#writeEntityHeader(String,int) writeEntityHeader()}).</li> - </ol> - <p>For example, the following code flattens some data into a byte stream and writes it into a -single entity:</p> - <pre> -// Create buffer stream and data output stream for our data -ByteArrayOutputStream bufStream = new ByteArrayOutputStream(); -DataOutputStream outWriter = new DataOutputStream(bufStream); -// Write structured data -outWriter.writeUTF(mPlayerName); -outWriter.writeInt(mPlayerScore); -// Send the data to the Backup Manager via the BackupDataOutput -byte[] buffer = bufStream.toByteArray(); -int len = buffer.length; -data.writeEntityHeader(TOPSCORE_BACKUP_KEY, len); -data.writeEntityData(buffer, len); -</pre> - <p>Perform this for each piece of data that you want to back up. How you divide your data into -entities is up to you (and you might use just one entity).</p> - </li> - <li>Whether or not you perform a backup (in step 2), write a representation of the current data to -the {@code newState} {@link android.os.ParcelFileDescriptor}. The Backup Manager retains this object -locally as a representation of the data that is currently backed up. It passes this back to you as -{@code oldState} the next time it calls {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} so you can determine whether another backup is necessary (as handled in step 1). If you -do not write the current data state to this file, then -{@code oldState} will be empty during the next callback. - <p>The following example saves a representation of the current data into {@code newState} using -the file's last-modified timestamp:</p> - <pre> -FileOutputStream outstream = new FileOutputStream(newState.getFileDescriptor()); -DataOutputStream out = new DataOutputStream(outstream); - -long modified = mDataFile.lastModified(); -out.writeLong(modified); -</pre> - </li> -</ol> - -<p class="caution"><strong>Caution:</strong> If your application data is saved to a file, make sure -that you use synchronized statements while accessing the file so that your backup agent does not -read the file while an Activity in your application is also writing the file.</p> - - - - -<h3 id="PerformingRestore">Performing restore</h3> - -<p>When it's time to restore your application data, the Backup Manager calls your backup -agent's {@link android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) -onRestore()} method. When it calls this method, the Backup Manager delivers your backup data so -you can restore it onto the device.</p> - -<p>Only the Backup Manager can call {@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) -onRestore()}, which happens automatically when the system installs your application and -finds existing backup data. However, you can request a restore operation for -your application by calling {@link -android.app.backup.BackupManager#requestRestore(RestoreObserver) requestRestore()} (see <a -href="#RequestingRestore">Requesting restore</a> for more information).</p> - -<p class="note"><strong>Note:</strong> While developing your application, you can also request a -restore operation with the <a href="{@docRoot}tools/help/bmgr.html">{@code bmgr} -tool</a>.</p> - -<p>When the Backup Manager calls your {@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) -onRestore()} method, it passes three parameters:</p> - -<dl> - <dt>{@code data}</dt> - <dd>A {@link android.app.backup.BackupDataInput}, which allows you to read your backup -data.</dd> - <dt>{@code appVersionCode}</dt> - <dd>An integer representing the value of your application's <a -href="{@docRoot}guide/topics/manifest/manifest-element.html#vcode">{@code android:versionCode}</a> -manifest attribute, as it was when this data was backed up. You can use this to cross-check the -current application version and determine if the data format is compatible. For more -information about using this to handle different versions of restore data, see the section -below about <a href="#RestoreVersion">Checking the Restore Data Version</a>.</dd> - <dt>{@code newState}</dt> - <dd>An open, read/write {@link android.os.ParcelFileDescriptor} pointing to a file in which -you must write the final backup state that was provided with {@code data}. This object is -returned as {@code oldState} the next time {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} is called. Recall that you must also write the same {@code newState} object in the -{@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} callback—also doing it here ensures that the {@code oldState} object given to -{@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} is valid even the first time {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} is called after the device is restored.</dd> -</dl> - -<p>In your implementation of {@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) -onRestore()}, you should call {@link android.app.backup.BackupDataInput#readNextHeader()} on the -{@code data} to iterate -through all entities in the data set. For each entity found, do the following:</p> - -<ol> - <li>Get the entity key with {@link android.app.backup.BackupDataInput#getKey()}.</li> - <li>Compare the entity key to a list of known key values that you should have declared as static -final strings inside your {@link android.app.backup.BackupAgent} class. When the key matches one of -your known key strings, enter into a statement to extract the entity data and save it to the device: - <ol> - <li>Get the entity data size with {@link -android.app.backup.BackupDataInput#getDataSize()} and create a byte array of that size.</li> - <li>Call {@link android.app.backup.BackupDataInput#readEntityData(byte[],int,int) -readEntityData()} and pass it the byte array, which is where the data will go, and specify the -start offset and the size to read.</li> - <li>Your byte array is now full and you can read the data and write it to the device -however you like.</li> - </ol> - </li> - <li>After you read and write your data back to the device, write the state of your data to the -{@code newState} parameter the same as you do during {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()}. -</ol> +page.title=Backing up App Data to the Cloud +page.tags=cloud,sync,backup -<p>For example, here's how you can restore the data backed up by the example in the previous -section:</p> - -<pre> -@Override -public void onRestore(BackupDataInput data, int appVersionCode, - ParcelFileDescriptor newState) throws IOException { - // There should be only one entity, but the safest - // way to consume it is using a while loop - while (data.readNextHeader()) { - String key = data.getKey(); - int dataSize = data.getDataSize(); - - // If the key is ours (for saving top score). Note this key was used when - // we wrote the backup entity header - if (TOPSCORE_BACKUP_KEY.equals(key)) { - // Create an input stream for the BackupDataInput - byte[] dataBuf = new byte[dataSize]; - data.readEntityData(dataBuf, 0, dataSize); - ByteArrayInputStream baStream = new ByteArrayInputStream(dataBuf); - DataInputStream in = new DataInputStream(baStream); - - // Read the player name and score from the backup data - mPlayerName = in.readUTF(); - mPlayerScore = in.readInt(); - - // Record the score on the device (to a file or something) - recordScore(mPlayerName, mPlayerScore); - } else { - // We don't know this entity key. Skip it. (Shouldn't happen.) - data.skipEntityData(); - } - } - - // Finally, write to the state blob (newState) that describes the restored data - FileOutputStream outstream = new FileOutputStream(newState.getFileDescriptor()); - DataOutputStream out = new DataOutputStream(outstream); - out.writeUTF(mPlayerName); - out.writeInt(mPlayerScore); -} -</pre> - -<p>In this example, the {@code appVersionCode} parameter passed to {@link -android.app.backup.BackupAgent#onRestore onRestore()} is not used. However, you might want to use -it if you've chosen to perform backup when the user's version of the application has actually moved -backward (for example, the user went from version 1.5 of your app to 1.0). For more information, see -the section about <a href="#RestoreVersion">Checking the Restore Data Version</a>.</p> - -<div class="special"> -<p>For an example implementation of {@link android.app.backup.BackupAgent}, see the <a -href="{@docRoot}resources/samples/BackupRestore/src/com/example/android/backuprestore/ExampleAgent.html">{@code -ExampleAgent}</a> class in the <a -href="{@docRoot}resources/samples/BackupRestore/index.html">Backup and Restore</a> sample -application.</p> -</div> - - - - - - -<h2 id="BackupAgentHelper">Extending BackupAgentHelper</h2> - -<p>You should build your backup agent using {@link android.app.backup.BackupAgentHelper} if you want -to back up complete files (from either {@link android.content.SharedPreferences} or <a -href="{@docRoot}guide/topics/data/data-storage.html#filesInternal">internal storage</a>). -Building your backup agent with {@link android.app.backup.BackupAgentHelper} requires far less -code than extending {@link android.app.backup.BackupAgent}, because you don't have to implement -{@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} and {@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) -onRestore()}.</p> - -<p>Your implementation of {@link android.app.backup.BackupAgentHelper} must -use one or more backup helpers. A backup helper is a specialized -component that {@link android.app.backup.BackupAgentHelper} summons to perform backup and -restore operations for a particular type of data. The Android framework currently provides two -different helpers:</p> -<ul> - <li>{@link android.app.backup.SharedPreferencesBackupHelper} to backup {@link -android.content.SharedPreferences} files.</li> - <li>{@link android.app.backup.FileBackupHelper} to backup files from <a -href="{@docRoot}guide/topics/data/data-storage.html#filesInternal">internal storage</a>.</li> -</ul> - -<p>You can include multiple helpers in your {@link android.app.backup.BackupAgentHelper}, but only -one helper is needed for each data type. That is, if you have multiple {@link -android.content.SharedPreferences} files, then you need only one {@link -android.app.backup.SharedPreferencesBackupHelper}.</p> - -<p>For each helper you want to add to your {@link android.app.backup.BackupAgentHelper}, you must do -the following during your {@link android.app.backup.BackupAgent#onCreate()} method:</p> -<ol> - <li>Instantiate in instance of the desired helper class. In the class constructor, you must -specify the appropriate file(s) you want to backup.</li> - <li>Call {@link android.app.backup.BackupAgentHelper#addHelper(String,BackupHelper) addHelper()} -to add the helper to your {@link android.app.backup.BackupAgentHelper}.</li> -</ol> - -<p>The following sections describe how to create a backup agent using each of the available -helpers.</p> - - - -<h3 id="SharedPreferences">Backing up SharedPreferences</h3> - -<p>When you instantiate a {@link android.app.backup.SharedPreferencesBackupHelper}, you must -include the name of one or more {@link android.content.SharedPreferences} files.</p> - -<p>For example, to back up a {@link android.content.SharedPreferences} file named -"user_preferences", a complete backup agent using {@link android.app.backup.BackupAgentHelper} looks -like this:</p> - -<pre> -public class MyPrefsBackupAgent extends BackupAgentHelper { - // The name of the SharedPreferences file - static final String PREFS = "user_preferences"; - - // A key to uniquely identify the set of backup data - static final String PREFS_BACKUP_KEY = "prefs"; - - // Allocate a helper and add it to the backup agent - @Override - public void onCreate() { - SharedPreferencesBackupHelper helper = - new SharedPreferencesBackupHelper(this, PREFS); - addHelper(PREFS_BACKUP_KEY, helper); - } -} -</pre> - -<p>That's it! That's your entire backup agent. The {@link -android.app.backup.SharedPreferencesBackupHelper} includes all the code -needed to backup and restore a {@link android.content.SharedPreferences} file.</p> - -<p>When the Backup Manager calls {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} and {@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) -onRestore()}, {@link android.app.backup.BackupAgentHelper} calls your backup helpers to perform -backup and restore for your specified files.</p> - -<p class="note"><strong>Note:</strong> {@link android.content.SharedPreferences} are threadsafe, so -you can safely read and write the shared preferences file from your backup agent and -other activities.</p> - - - -<h3 id="Files">Backing up other files</h3> - -<p>When you instantiate a {@link android.app.backup.FileBackupHelper}, you must include the name of -one or more files that are saved to your application's <a -href="{@docRoot}guide/topics/data/data-storage.html#filesInternal">internal storage</a> -(as specified by {@link android.content.ContextWrapper#getFilesDir()}, which is the same -location where {@link android.content.Context#openFileOutput(String,int) openFileOutput()} writes -files).</p> - -<p>For example, to backup two files named "scores" and "stats," a backup agent using {@link -android.app.backup.BackupAgentHelper} looks like this:</p> - -<pre> -public class MyFileBackupAgent extends BackupAgentHelper { - // The name of the file - static final String TOP_SCORES = "scores"; - static final String PLAYER_STATS = "stats"; - - // A key to uniquely identify the set of backup data - static final String FILES_BACKUP_KEY = "myfiles"; - - // Allocate a helper and add it to the backup agent - @Override - public void onCreate() { - FileBackupHelper helper = new FileBackupHelper(this, - TOP_SCORES, PLAYER_STATS); - addHelper(FILES_BACKUP_KEY, helper); - } -} -</pre> - -<p>The {@link android.app.backup.FileBackupHelper} includes all the code necessary to backup and -restore files that are saved to your application's <a -href="{@docRoot}guide/topics/data/data-storage.html#filesInternal">internal storage</a>..</p> - -<p>However, reading and writing to files on internal storage is <strong>not threadsafe</strong>. To -ensure that your backup agent does not read or write your files at the same time as your activities, -you must use synchronized statements each time you perform a read or write. For example, -in any Activity where you read and write the file, you need an object to use as the intrinsic -lock for the synchronized statements:</p> - -<pre> -// Object for intrinsic lock -static final Object sDataLock = new Object(); -</pre> - -<p>Then create a synchronized statement with this lock each time you read or write the files. For -example, here's a synchronized statement for writing the latest score in a game to a file:</p> - -<pre> -try { - synchronized (MyActivity.sDataLock) { - File dataFile = new File({@link android.content.Context#getFilesDir()}, TOP_SCORES); - RandomAccessFile raFile = new RandomAccessFile(dataFile, "rw"); - raFile.writeInt(score); - } -} catch (IOException e) { - Log.e(TAG, "Unable to write to file"); -} -</pre> - -<p>You should synchronize your read statements with the same lock.</p> - -<p>Then, in your {@link android.app.backup.BackupAgentHelper}, you must override {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} and {@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) -onRestore()} to synchronize the backup and restore operations with the same -intrinsic lock. For example, the {@code MyFileBackupAgent} example from above needs the following -methods:</p> - -<pre> -@Override -public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, - ParcelFileDescriptor newState) throws IOException { - // Hold the lock while the FileBackupHelper performs backup - synchronized (MyActivity.sDataLock) { - super.onBackup(oldState, data, newState); - } -} - -@Override -public void onRestore(BackupDataInput data, int appVersionCode, - ParcelFileDescriptor newState) throws IOException { - // Hold the lock while the FileBackupHelper restores the file - synchronized (MyActivity.sDataLock) { - super.onRestore(data, appVersionCode, newState); - } -} -</pre> - -<p>That's it. All you need to do is add your {@link android.app.backup.FileBackupHelper} in the -{@link android.app.backup.BackupAgent#onCreate()} method and override {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} and {@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) -onRestore()} to synchronize read and write operations.</p> - -<div class="special"> -<p>For an example implementation of {@link -android.app.backup.BackupAgentHelper} with {@link android.app.backup.FileBackupHelper}, see the -{@code FileHelperExampleAgent} class in the <a -href="{@docRoot}resources/samples/BackupRestore/index.html">Backup and Restore</a> sample -application.</p> -</div> - - - - - - -<h2 id="RestoreVersion">Checking the Restore Data Version</h2> - -<p>When the Backup Manager saves your data to cloud storage, it automatically includes the version -of your application, as defined by your manifest file's <a -href="{@docRoot}guide/topics/manifest/manifest-element.html#vcode">{@code android:versionCode}</a> -attribute. Before the Backup Manager calls your backup agent to restore your data, it -looks at the <a -href="{@docRoot}guide/topics/manifest/manifest-element.html#vcode">{@code -android:versionCode}</a> of the installed application and compares it to the value -recorded in the restore data set. If the version recorded in the restore data set is -<em>newer</em> than the application version on the device, then the user has downgraded their -application. In this case, the Backup Manager will abort the restore operation for your application -and not call your {@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) onRestore()} -method, because the restore set is considered meaningless to an older version.</p> - -<p>You can override this behavior with the <a -href="{@docRoot}guide/topics/manifest/application-element.html#restoreany">{@code -android:restoreAnyVersion}</a> attribute. This attribute is either "{@code true}" or "{@code -false}" to indicate whether you want to restore the application regardless of the restore set -version. The default value is "{@code false}". If you define this to be "{@code true}" then the -Backup Manager will ignore the <a -href="{@docRoot}guide/topics/manifest/manifest-element.html#vcode">{@code android:versionCode}</a> -and call your {@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) onRestore()} -method in all cases. In doing so, you can manually check for the version difference in your {@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) onRestore()} -method and take any steps necessary to make the data compatible if the versions conflict.</p> - -<p>To help you handle different versions during a restore operation, the {@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) onRestore()} -method passes you the version code included with the restore data set as the {@code appVersionCode} -parameter. You can then query the current application's version code with the {@link -android.content.pm.PackageInfo#versionCode PackageInfo.versionCode} field. For example:</p> - -<pre> -PackageInfo info; -try { - String name = {@link android.content.ContextWrapper#getPackageName() getPackageName}(); - info = {@link android.content.ContextWrapper#getPackageManager -getPackageManager}().{@link android.content.pm.PackageManager#getPackageInfo(String,int) -getPackageInfo}(name,0); -} catch (NameNotFoundException nnfe) { - info = null; -} - -int version; -if (info != null) { - version = info.versionCode; -} -</pre> - -<p>Then simply compare the {@code version} acquired from {@link android.content.pm.PackageInfo} -to the {@code appVersionCode} passed into {@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) onRestore()}. -</p> - -<p class="caution"><strong>Caution:</strong> Be certain you understand the consequences of setting -<a href="{@docRoot}guide/topics/manifest/application-element.html#restoreany">{@code -android:restoreAnyVersion}</a> to "{@code true}" for your application. If each version of your -application that supports backup does not properly account for variations in your data format during -{@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) onRestore()}, -then the data on the device could be saved in a format incompatible with the version currently -installed on the device.</p> - - - -<h2 id="RequestingBackup">Requesting Backup</h2> - -<p>You can request a backup operation at any time by calling {@link -android.app.backup.BackupManager#dataChanged()}. This method notifies the Backup Manager that you'd -like to backup your data using your backup agent. The Backup Manager then calls your backup -agent's {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()} method at an opportune time in the future. Typically, you should -request a backup each time your data changes (such as when the user changes an application -preference that you'd like to back up). If you call {@link -android.app.backup.BackupManager#dataChanged()} several times consecutively, before the Backup -Manager requests a backup from your agent, your agent still receives just one call to {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) -onBackup()}.</p> - -<p class="note"><strong>Note:</strong> While developing your application, you can request a -backup and initiate an immediate backup operation with the <a -href="{@docRoot}tools/help/bmgr.html">{@code bmgr} -tool</a>.</p> - - -<h2 id="RequestingRestore">Requesting Restore</h2> - -<p>During the normal life of your application, you shouldn't need to request a restore operation. -They system automatically checks for backup data and performs a restore when your application is -installed. However, you can manually request a restore operation by calling {@link -android.app.backup.BackupManager#requestRestore(RestoreObserver) requestRestore()}, if necessary. In -which case, the Backup Manager calls your {@link -android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) onRestore()} -implementation, passing the data from the current set of backup data.</p> - -<p class="note"><strong>Note:</strong> While developing your application, you can request a -restore operation with the <a href="{@docRoot}tools/help/bmgr.html">{@code bmgr} -tool</a>.</p> - - -<h2 id="Testing">Testing Your Backup Agent</h2> - -<p>Once you've implemented your backup agent, you can test the backup and restore functionality -with the following procedure, using <a -href="{@docRoot}tools/help/bmgr.html">{@code bmgr}</a>.</p> - -<ol> - <li>Install your application on a suitable Android system image - <ul> - <li>If using the emulator, create and use an AVD with Android 2.2 (API Level 8).</li> - <li>If using a device, the device must be running Android 2.2 or greater and have Google -Play built in.</li> - </ul> - </li> - <li>Ensure that backup is enabled - <ul> - <li>If using the emulator, you can enable backup with the following command from your SDK -{@code tools/} path: -<pre class="no-pretty-print">adb shell bmgr enable true</pre> - </li> - <li>If using a device, open the system <b>Settings</b>, select - <b>Backup & reset</b>, then enable - <b>Back up my data</b> and <b>Automatic restore</b>.</li> - </ul> - </li> - <li>Open your application and initialize some data - <p>If you've properly implemented backup in your application, then it should request a -backup each time the data changes. For example, each time the user changes some data, your app -should call {@link android.app.backup.BackupManager#dataChanged()}, which adds a backup request to -the Backup Manager queue. For testing purposes, you can also make a request with the following -{@code bmgr} command:</p> -<pre class="no-pretty-print">adb shell bmgr backup <em>your.package.name</em></pre> - </li> - <li>Initiate a backup operation: -<pre class="no-pretty-print">adb shell bmgr run</pre> - <p>This forces the Backup Manager to perform all backup requests that are in its -queue.</p> - <li>Uninstall your application: -<pre class="no-pretty-print">adb uninstall <em>your.package.name</em></pre> - </li> - <li>Re-install your application.</li> -</ol> - -<p>If your backup agent is successful, all the data you initialized in step 4 is restored.</p> +startpage=true +@jd:body +<p>Users often invest significant time and effort creating data and setting +preferences within apps. Preserving that data for users if they replace a broken +device or upgrade to a new one is an important part of ensuring a great user +experience. This section covers techniques for backing up data to the cloud so +that users can restore their data. + +<p>Android provides two ways for apps to backup their data to the cloud: +<a href="{@docRoot}guide/topics/data/autobackup.html">Auto Backup for Apps</a> and +<a href="{@docRoot}guide/topics/data/keyvaluebackup.html">Key/Value Backup</a>. +Auto Backup, which is available starting API 23, preserves app data by uploading +it to the user’s Google Drive account. The Key/Value Backup feature (formerly +known as the Backup API and the Android Backup Service) preserves app data by +uploading it to the <a href="{@docRoot}google/backup/index.html">Android Backup Service</a>. + +<p>Generally, we recommend Auto Backup because it requires no work to implement. +Apps that target Android 6.0 (API level 23) or higher are automatically enabled +for Auto Backup. The Auto Backup feature does have some limitations in terms of +what data it can backup and it's availability on Android 6.0 and higher devices. +Consider using the Key/Value Backup feature if you have more specific needs for +backing up your app data. For more information, see <a href="{@docRoot}guide/topics/data/keyvaluebackup.html#Comparison">Comparison of Key/Value and Auto Backup</a></p> + +<p class="note"><strong>Note:</strong> These data backup features are not designed for synchronizing app data with other clients or +saving data that you'd like to access during the normal application lifecycle. +You cannot read or write backup data on demand. For synchronizing app data, see +<a href="{@docRoot}training/sync-adapters/index.html">Transferring +Data Using Sync Adapters</a> or <a href="https://developers.google.com/drive/android/">Google Drive Android +API</a>.
\ No newline at end of file diff --git a/docs/html/guide/topics/data/images/backup-framework.png b/docs/html/guide/topics/data/images/backup-framework.png Binary files differnew file mode 100644 index 000000000000..2ba2e612c9bd --- /dev/null +++ b/docs/html/guide/topics/data/images/backup-framework.png diff --git a/docs/html/guide/topics/data/index.jd b/docs/html/guide/topics/data/index.jd index 3872825f1619..2365f18aaeb8 100644 --- a/docs/html/guide/topics/data/index.jd +++ b/docs/html/guide/topics/data/index.jd @@ -5,21 +5,3 @@ page.landing.image= @jd:body -<div class="landing-docs"> - - - <div class="col-12"> - <h3>Training</h3> - - <a href="{@docRoot}training/backup/index.html"> - <h4>Backing up App Data to the Cloud</h4> - <p> - This class covers techniques for backing up data to the cloud so that - users can restore their data when recovering from a data loss (such as a - factory reset) or installing your application on a new device. - </p> - </a> - - </div> - -</div> diff --git a/docs/html/guide/topics/data/keyvaluebackup.jd b/docs/html/guide/topics/data/keyvaluebackup.jd new file mode 100644 index 000000000000..c7c5e2fd0ce1 --- /dev/null +++ b/docs/html/guide/topics/data/keyvaluebackup.jd @@ -0,0 +1,884 @@ +page.title=Key/Value Backup +page.tags=backup, marshmallow, androidm +page.keywords=backup, kvbackup + +@jd:body + +<div id="qv-wrapper"> +<div id="qv"> + <h2>In this document</h2> + <ol> + <li><a href="#Comparison">Comparison to Auto Backup</a></li> + <li><a href="#ImplementingBackup">Implementing Key/Value Backup</a></li> + + <li><a href="#BackupManifest">Declaring the backup agent in your manifest</a></li> + <li><a href="#BackupKey">Registering for Android Backup Service</a></li> + <li><a href="#BackupAgent">Extending BackupAgent</a> + <ol> + <li><a href="#RequiredMethods">Required methods</a></li> + <li><a href="#PerformingBackup">Performing backup</a></li> + <li><a href="#PerformingRestore">Performing restore</a></li> + </ol> + </li> + <li><a href="#BackupAgentHelper">Extending BackupAgentHelper</a> + <ol> + <li><a href="#SharedPreferences">Backing up SharedPreferences</a></li> + <li><a href="#Files">Backing up private files</a></li> + </ol> + </li> + <li><a href="#RestoreVersion">Checking the restore data version</a></li> + <li><a href="#RequestingBackup">Requesting backup</a></li> + <li><a href="#RequestingRestore">Requesting restore</a></li> + <li><a href="#Migrating">Migrating to Auto Backup</a></li> + </ol> + + <h2>Key classes</h2> + <ol> + <li>{@link android.app.backup.BackupManager}</li> + <li>{@link android.app.backup.BackupAgent}</li> + <li>{@link android.app.backup.BackupAgentHelper}</li> + </ol> + +</div> +</div> + + +<p>Since Android 2.2 (API 8), Android has offered the <em>Key/Value Backup</em> +feature as a way for developers to backup app data to the cloud. The Key/Value +Backup feature (formerly known as the Backup API and the Android Backup Service) +preserves app data by uploading it to +<a href="{@docRoot}google/backup/index.html">Android Backup Service</a>. +The amount of data is limited to 5MB per user of your app and there is +no charge for storing backup data. + +<p class="note"><strong>Note:</strong> If your app implements Key/Value Backup +and targets API 23 or higher, you should set +<a href="{@docRoot}reference/android/R.attr.html#fullBackupOnly">android:fullBackupOnly</a>. +This attribute indicates whether or not to use Auto Backup on devices where it is available. + +<h2 id="Comparison">Comparison to Auto Backup</h2> +<p>Like Auto Backup, Key/Value Backups are restored automatically whenever the app +is installed. The following table describes some of the key differences between Key/Value Backup and Auto Backup: + +<table> + <tr> + <th>Key/Value Backup</th> + <th>Auto Backup</th> + </tr> + <tr> + <td>Available in API 8, Android 2.2</td> + <td>Available in API 23, Android 6.0</td> + </tr> + <tr> + <td>Apps must implement a {@link android.app.backup.BackupAgent}. The backup agent defines what data to backup and how to +restore data.</td> + <td>By default, Auto Backup includes almost all of the app's files. You can +use XML to include and exclude files. Under the hood, Auto Backup relies on a +backup agent that is built into the framework.</td> + </tr> + <tr> + <td>Apps must issue a request when there is data +that is ready to be backed up. Requests +from multiple apps are batched and executed every few hours.</td> + <td>Backups happen automatically roughly once a day.</td> + </tr> + <tr> + <td>Backup data can be transmitted via wifi or cellular data.</td> + <td>Backup data is tranmitted only via wifi. If the device is never connected to a +wifi network, then Auto Backup never occurs.</td> + </tr> + <tr> + <td>Apps are not shut down during backup.</td> + <td>The system shuts down the app during backup.</td> + </tr> + <tr> + <td>Backup data is stored in <a href="{@docRoot}google/backup/index.html">Android Backup Service</a> limited to 5MB per app.</td> + <td>Backup data is stored in the user's Google Drive limited to 25MB per app.</td> + </tr> + <tr> + <td>Related API methods are not filed based:<ul> + <li>{@link android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor) onBackup()} + <li>{@link android.app.backup.BackupAgent#onRestore(BackupDataInput, int, ParcelFileDescriptor) onRestore()}</ul></td> + <td>Related API methods are filed based:<ul> +<li>{@link android.app.backup.BackupAgent#onFullBackup(FullBackupDataOutput) onFullBackup()} +<li>{@link android.app.backup.BackupAgent#onRestoreFile(ParcelFileDescriptor,long,File,int,long,long) onRestoreFile()}</ul></td> + </tr> +</table> + +<h2 id="ImplementingBackup">Implementing Key/Value Backup</h2> +<p>To backup your application data, you need to implement a backup agent. Your backup +agent is called by the Backup Manager both during backup and restore.</p> + +<p>To implement a backup agent, you must:</p> + +<ol> + <li>Declare your backup agent in your manifest file with the <a +href="{@docRoot}guide/topics/manifest/application-element.html#agent">{@code +android:backupAgent}</a> attribute.</li> + <li>Register your application with <a href="{@docRoot}google/backup/index.html">Android + Backup Service</a></li> + <li>Define a backup agent by either:</p> + <ol type="a"> + <li><a href="#BackupAgent">Extending BackupAgent</a> + <p>The {@link android.app.backup.BackupAgent} class provides the central interface with +which your application communicates with the Backup Manager. If you extend this class +directly, you must override {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} and {@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) +onRestore()} to handle the backup and restore operations for your data.</p> + <p><em>Or</em></p> + <li><a href="#BackupAgentHelper">Extending BackupAgentHelper</a> + <p>The {@link android.app.backup.BackupAgentHelper} class provides a convenient +wrapper around the {@link android.app.backup.BackupAgent} class, which minimizes the amount of code +you need to write. In your {@link android.app.backup.BackupAgentHelper}, you must use one or more +"helper" objects, which automatically backup and restore certain types of data, so that you do not +need to implement {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} and {@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) +onRestore()}.</p> + <p>Android currently provides backup helpers that will backup and restore complete files +from {@link android.content.SharedPreferences} and <a +href="{@docRoot}guide/topics/data/data-storage.html#filesInternal">internal storage</a>.</p> + </li> + </ol> + </li> +</ol> + +<h2 id="BackupManifest">Declaring the backup agent in your manifest</h2> + +<p>This is the easiest step, so once you've decided on the class name for your backup agent, declare +it in your manifest with the <a +href="{@docRoot}guide/topics/manifest/application-element.html#agent">{@code +android:backupAgent}</a> attribute in the <a +href="{@docRoot}guide/topics/manifest/application-element.html">{@code +<application>}</a> tag.</p> + +<p>For example:</p> + +<pre> +<manifest ... > + ... + <application android:label="MyApplication" + <b>android:backupAgent="MyBackupAgent"</b>> + <activity ... > + ... + </activity> + </application> +</manifest> +</pre> + +<p>Another attribute you might want to use is <a +href="{@docRoot}guide/topics/manifest/application-element.html#restoreany">{@code +android:restoreAnyVersion}</a>. This attribute takes a boolean value to indicate whether you +want to restore the application data regardless of the current application version compared to the +version that produced the backup data. (The default value is "{@code false}".) See <a +href="#RestoreVersion">Checking the Restore Data Version</a> for more information.</p> + +<p class="note"><strong>Note:</strong> The backup service and the APIs you must use are +available only on devices running API Level 8 (Android 2.2) or greater, so you should also +set your <a +href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#min">{@code android:minSdkVersion}</a> +attribute to "8".</p> + + + + +<h2 id="BackupKey">Registering for Android Backup Service</h2> + +<p>Google provides a backup transport with <a +href="{@docRoot}google/backup/index.html">Android Backup Service</a> for most +Android-powered devices running Android 2.2 or greater.</p> + +<p>In order for your application to perform backup using Android Backup Service, you must +register your application with the service to receive a Backup Service Key, then +declare the Backup Service Key in your Android manifest.</p> + +<p>To get your Backup Service Key, <a +href="{@docRoot}google/backup/signup.html">register for Android Backup Service</a>. +When you register, you will be provided a Backup Service Key and the appropriate {@code +<meta-data>} XML code for your Android manifest file, which you must include as a child of the +{@code <application>} element. For example:</p> + +<pre> +<application android:label="MyApplication" + android:backupAgent="MyBackupAgent"> + ... + <meta-data android:name="com.google.android.backup.api_key" + android:value="AEdPqrEAAAAIDaYEVgU6DJnyJdBmU7KLH3kszDXLv_4DIsEIyQ" /> +</application> +</pre> + +<p>The <code>android:name</code> must be <code>"com.google.android.backup.api_key"</code> and +the <code>android:value</code> must be the Backup Service Key received from the Android Backup +Service registration.</p> + +<p>If you have multiple applications, you must register each one, using the respective package +name.</p> + +<p class="note"><strong>Note:</strong> The backup transport provided by Android Backup Service is +not guaranteed to be available +on all Android-powered devices that support backup. Some devices might support backup +using a different transport, some devices might not support backup at all, and there is no way for +your application to know what transport is used on the device. However, if you implement backup for +your application, you should always include a Backup Service Key for Android Backup Service so +your application can perform backup when the device uses the Android Backup Service transport. If +the device does not use Android Backup Service, then the {@code <meta-data>} element with the +Backup Service Key is ignored.</p> + + + + +<h2 id="BackupAgent">Extending BackupAgent</h2> + +<p>Most applications shouldn't need to extend the {@link android.app.backup.BackupAgent} class +directly, but should instead <a href="#BackupAgentHelper">extend BackupAgentHelper</a> to take +advantage of the built-in helper classes that automatically backup and restore your files. However, +you might want to extend {@link android.app.backup.BackupAgent} directly if you need to:</p> +<ul> + <li>Version your data format. For instance, if you anticipate the need to revise the +format in which you write your application data, you can build a backup agent to cross-check your +application version during a restore operation and perform any necessary compatibility work if the +version on the device is different than that of the backup data. For more information, see <a +href="#RestoreVersion">Checking the Restore Data Version</a>.</li> + <li>Instead of backing up an entire file, you can specify the portions of data the should be +backed up and how each portion is then restored to the device. (This can also help you manage +different versions, because you read and write your data as unique entities, rather than +complete files.)</li> + <li>Back up data in a database. If you have an SQLite database that you want to restore when +the user re-installs your application, you need to build a custom {@link +android.app.backup.BackupAgent} that reads the appropriate data during a backup operation, then +create your table and insert the data during a restore operation.</li> +</ul> + +<p>If you don't need to perform any of the tasks above and want to back up complete files from +{@link android.content.SharedPreferences} or <a +href="{@docRoot}guide/topics/data/data-storage.html#filesInternal">internal storage</a>, you +should skip to <a href="#BackupAgentHelper">Extending BackupAgentHelper</a>.</p> + + + +<h3 id="RequiredMethods">Required methods</h3> + +<p>When you create a backup agent by extending {@link android.app.backup.BackupAgent}, you +must implement the following callback methods:</p> + +<dl> + <dt>{@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()}</dt> + <dd>The Backup Manager calls this method after you <a href="#RequestingBackup">request a +backup</a>. In this method, you read your application data from the device and pass the data you +want to back up to the Backup Manager, as described below in <a href="#PerformingBackup">Performing +backup</a>.</dd> + + <dt>{@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) +onRestore()}</dt> + <dd>The Backup Manager calls this method during a restore operation (you can <a +href="#RequestingRestore">request a restore</a>, but the system automatically performs restore when +the user re-installs your application). When it calls this method, the Backup Manager delivers your +backup data, which you then restore to the device, as described below in <a +href="#PerformingRestore">Performing restore</a>.</dd> +</dl> + + + +<h3 id="PerformingBackup">Performing backup</h3> + + +<p>When it's time to back up your application data, the Backup Manager calls your {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} method. This is where you must provide your application data to the Backup Manager so +it can be saved to cloud storage.</p> + +<p>Only the Backup Manager can call your backup agent's {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} method. Each time that your application data changes and you want to perform a backup, +you must request a backup operation by calling {@link +android.app.backup.BackupManager#dataChanged()} (see <a href="#RequestingBackup">Requesting +Backup</a> for more information). A backup request does not result in an immediate call to your +{@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} method. Instead, the Backup Manager waits for an appropriate time, then performs +backup for all applications that have requested a backup since the last backup was performed.</p> + +<p class="note"><strong>Tip:</strong> While developing your application, you can initiate an +immediate backup operation from the Backup Manager with the <a +href="{@docRoot}tools/help/bmgr.html">{@code bmgr} tool</a>.</p> + +<p>When the Backup Manager calls your {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} method, it passes three parameters:</p> + +<dl> + <dt>{@code oldState}</dt> + <dd>An open, read-only {@link android.os.ParcelFileDescriptor} pointing to the last backup +state provided by your application. This is not the backup data from cloud storage, but a +local representation of the data that was backed up the last time {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} was called (as defined by {@code newState}, below, or from {@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) +onRestore()}—more about this in the next section). Because {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} does not allow you to read existing backup data in +the cloud storage, you can use this local representation to determine whether your data has changed +since the last backup.</dd> + <dt>{@code data}</dt> + <dd>A {@link android.app.backup.BackupDataOutput} object, which you use to deliver your backup +data to the Backup Manager.</dd> + <dt>{@code newState}</dt> + <dd>An open, read/write {@link android.os.ParcelFileDescriptor} pointing to a file in which +you must write a representation of the data that you delivered to {@code data} (a representation +can be as simple as the last-modified timestamp for your file). This object is +returned as {@code oldState} the next time the Backup Manager calls your {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} method. If you do not write your backup data to {@code newState}, then {@code oldState} +will point to an empty file next time Backup Manager calls {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()}.</dd> +</dl> + +<p>Using these parameters, you should implement your {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} method to do the following:</p> + +<ol> + <li>Check whether your data has changed since the last backup by comparing {@code oldState} to +your current data. How you read data in {@code oldState} depends on how you originally wrote it to +{@code newState} (see step 3). The easiest way to record the state of a file is with its +last-modified timestamp. For example, here's how you can read and compare a timestamp from {@code +oldState}: + <pre> +// Get the oldState input stream +FileInputStream instream = new FileInputStream(oldState.getFileDescriptor()); +DataInputStream in = new DataInputStream(instream); + +try { + // Get the last modified timestamp from the state file and data file + long stateModified = in.readLong(); + long fileModified = mDataFile.lastModified(); + + if (stateModified != fileModified) { + // The file has been modified, so do a backup + // Or the time on the device changed, so be safe and do a backup + } else { + // Don't back up because the file hasn't changed + return; + } +} catch (IOException e) { + // Unable to read state file... be safe and do a backup +} +</pre> + <p>If nothing has changed and you don't need to back up, skip to step 3.</p> + </li> + <li>If your data has changed, compared to {@code oldState}, write the current data to +{@code data} to back it up to the cloud storage. + <p>You must write each chunk of data as an "entity" in the {@link +android.app.backup.BackupDataOutput}. An entity is a flattened binary data +record that is identified by a unique key string. Thus, the data set that you back up is +conceptually a set of key-value pairs.</p> + <p>To add an entity to your backup data set, you must:</p> + <ol> + <li>Call {@link android.app.backup.BackupDataOutput#writeEntityHeader(String,int) +writeEntityHeader()}, passing a unique string key for the data you're about to write and the data +size.</li> + <li>Call {@link android.app.backup.BackupDataOutput#writeEntityData(byte[],int) +writeEntityData()}, passing a byte buffer that contains your data and the number of bytes to write +from the buffer (which should match the size passed to {@link +android.app.backup.BackupDataOutput#writeEntityHeader(String,int) writeEntityHeader()}).</li> + </ol> + <p>For example, the following code flattens some data into a byte stream and writes it into a +single entity:</p> + <pre> +// Create buffer stream and data output stream for our data +ByteArrayOutputStream bufStream = new ByteArrayOutputStream(); +DataOutputStream outWriter = new DataOutputStream(bufStream); +// Write structured data +outWriter.writeUTF(mPlayerName); +outWriter.writeInt(mPlayerScore); +// Send the data to the Backup Manager via the BackupDataOutput +byte[] buffer = bufStream.toByteArray(); +int len = buffer.length; +data.writeEntityHeader(TOPSCORE_BACKUP_KEY, len); +data.writeEntityData(buffer, len); +</pre> + <p>Perform this for each piece of data that you want to back up. How you divide your data into +entities is up to you (and you might use just one entity).</p> + </li> + <li>Whether or not you perform a backup (in step 2), write a representation of the current data to +the {@code newState} {@link android.os.ParcelFileDescriptor}. The Backup Manager retains this object +locally as a representation of the data that is currently backed up. It passes this back to you as +{@code oldState} the next time it calls {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} so you can determine whether another backup is necessary (as handled in step 1). If you +do not write the current data state to this file, then +{@code oldState} will be empty during the next callback. + <p>The following example saves a representation of the current data into {@code newState} using +the file's last-modified timestamp:</p> + <pre> +FileOutputStream outstream = new FileOutputStream(newState.getFileDescriptor()); +DataOutputStream out = new DataOutputStream(outstream); + +long modified = mDataFile.lastModified(); +out.writeLong(modified); +</pre> + </li> +</ol> + +<p class="caution"><strong>Caution:</strong> If your application data is saved to a file, make sure +that you use synchronized statements while accessing the file so that your backup agent does not +read the file while an Activity in your application is also writing the file.</p> + + + + +<h3 id="PerformingRestore">Performing restore</h3> + +<p>When it's time to restore your application data, the Backup Manager calls your backup +agent's {@link android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) +onRestore()} method. When it calls this method, the Backup Manager delivers your backup data so +you can restore it onto the device.</p> + +<p>Only the Backup Manager can call {@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) +onRestore()}, which happens automatically when the system installs your application and +finds existing backup data. However, you can request a restore operation for +your application by calling {@link +android.app.backup.BackupManager#requestRestore(RestoreObserver) requestRestore()} (see <a +href="#RequestingRestore">Requesting restore</a> for more information).</p> + +<p class="note"><strong>Note:</strong> While developing your application, you can also request a +restore operation with the <a href="{@docRoot}tools/help/bmgr.html">{@code bmgr} +tool</a>.</p> + +<p>When the Backup Manager calls your {@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) +onRestore()} method, it passes three parameters:</p> + +<dl> + <dt>{@code data}</dt> + <dd>A {@link android.app.backup.BackupDataInput}, which allows you to read your backup +data.</dd> + <dt>{@code appVersionCode}</dt> + <dd>An integer representing the value of your application's <a +href="{@docRoot}guide/topics/manifest/manifest-element.html#vcode">{@code android:versionCode}</a> +manifest attribute, as it was when this data was backed up. You can use this to cross-check the +current application version and determine if the data format is compatible. For more +information about using this to handle different versions of restore data, see the section +below about <a href="#RestoreVersion">Checking the Restore Data Version</a>.</dd> + <dt>{@code newState}</dt> + <dd>An open, read/write {@link android.os.ParcelFileDescriptor} pointing to a file in which +you must write the final backup state that was provided with {@code data}. This object is +returned as {@code oldState} the next time {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} is called. Recall that you must also write the same {@code newState} object in the +{@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} callback—also doing it here ensures that the {@code oldState} object given to +{@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} is valid even the first time {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} is called after the device is restored.</dd> +</dl> + +<p>In your implementation of {@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) +onRestore()}, you should call {@link android.app.backup.BackupDataInput#readNextHeader()} on the +{@code data} to iterate +through all entities in the data set. For each entity found, do the following:</p> + +<ol> + <li>Get the entity key with {@link android.app.backup.BackupDataInput#getKey()}.</li> + <li>Compare the entity key to a list of known key values that you should have declared as static +final strings inside your {@link android.app.backup.BackupAgent} class. When the key matches one of +your known key strings, enter into a statement to extract the entity data and save it to the device: + <ol> + <li>Get the entity data size with {@link +android.app.backup.BackupDataInput#getDataSize()} and create a byte array of that size.</li> + <li>Call {@link android.app.backup.BackupDataInput#readEntityData(byte[],int,int) +readEntityData()} and pass it the byte array, which is where the data will go, and specify the +start offset and the size to read.</li> + <li>Your byte array is now full and you can read the data and write it to the device +however you like.</li> + </ol> + </li> + <li>After you read and write your data back to the device, write the state of your data to the +{@code newState} parameter the same as you do during {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()}. +</ol> + +<p>For example, here's how you can restore the data backed up by the example in the previous +section:</p> + +<pre> +@Override +public void onRestore(BackupDataInput data, int appVersionCode, + ParcelFileDescriptor newState) throws IOException { + // There should be only one entity, but the safest + // way to consume it is using a while loop + while (data.readNextHeader()) { + String key = data.getKey(); + int dataSize = data.getDataSize(); + + // If the key is ours (for saving top score). Note this key was used when + // we wrote the backup entity header + if (TOPSCORE_BACKUP_KEY.equals(key)) { + // Create an input stream for the BackupDataInput + byte[] dataBuf = new byte[dataSize]; + data.readEntityData(dataBuf, 0, dataSize); + ByteArrayInputStream baStream = new ByteArrayInputStream(dataBuf); + DataInputStream in = new DataInputStream(baStream); + + // Read the player name and score from the backup data + mPlayerName = in.readUTF(); + mPlayerScore = in.readInt(); + + // Record the score on the device (to a file or something) + recordScore(mPlayerName, mPlayerScore); + } else { + // We don't know this entity key. Skip it. (Shouldn't happen.) + data.skipEntityData(); + } + } + + // Finally, write to the state blob (newState) that describes the restored data + FileOutputStream outstream = new FileOutputStream(newState.getFileDescriptor()); + DataOutputStream out = new DataOutputStream(outstream); + out.writeUTF(mPlayerName); + out.writeInt(mPlayerScore); +} +</pre> + +<p>In this example, the {@code appVersionCode} parameter passed to {@link +android.app.backup.BackupAgent#onRestore onRestore()} is not used. However, you might want to use +it if you've chosen to perform backup when the user's version of the application has actually moved +backward (for example, the user went from version 1.5 of your app to 1.0). For more information, see +the section about <a href="#RestoreVersion">Checking the Restore Data Version</a>.</p> + +<div class="special"> +<p>For an example implementation of {@link android.app.backup.BackupAgent}, see the <a +href="{@docRoot}resources/samples/BackupRestore/src/com/example/android/backuprestore/ExampleAgent.html">{@code +ExampleAgent}</a> class in the <a +href="{@docRoot}resources/samples/BackupRestore/index.html">Backup and Restore</a> sample +application.</p> +</div> + + + + + + +<h2 id="BackupAgentHelper">Extending BackupAgentHelper</h2> + +<p>You should build your backup agent using {@link android.app.backup.BackupAgentHelper} if you want +to back up complete files (from either {@link android.content.SharedPreferences} or <a +href="{@docRoot}guide/topics/data/data-storage.html#filesInternal">internal storage</a>). +Building your backup agent with {@link android.app.backup.BackupAgentHelper} requires far less +code than extending {@link android.app.backup.BackupAgent}, because you don't have to implement +{@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} and {@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) +onRestore()}.</p> + +<p>Your implementation of {@link android.app.backup.BackupAgentHelper} must +use one or more backup helpers. A backup helper is a specialized +component that {@link android.app.backup.BackupAgentHelper} summons to perform backup and +restore operations for a particular type of data. The Android framework currently provides two +different helpers:</p> +<ul> + <li>{@link android.app.backup.SharedPreferencesBackupHelper} to backup {@link +android.content.SharedPreferences} files.</li> + <li>{@link android.app.backup.FileBackupHelper} to backup files from <a +href="{@docRoot}guide/topics/data/data-storage.html#filesInternal">internal storage</a>.</li> +</ul> + +<p>You can include multiple helpers in your {@link android.app.backup.BackupAgentHelper}, but only +one helper is needed for each data type. That is, if you have multiple {@link +android.content.SharedPreferences} files, then you need only one {@link +android.app.backup.SharedPreferencesBackupHelper}.</p> + +<p>For each helper you want to add to your {@link android.app.backup.BackupAgentHelper}, you must do +the following during your {@link android.app.backup.BackupAgent#onCreate()} method:</p> +<ol> + <li>Instantiate in instance of the desired helper class. In the class constructor, you must +specify the appropriate file(s) you want to backup.</li> + <li>Call {@link android.app.backup.BackupAgentHelper#addHelper(String,BackupHelper) addHelper()} +to add the helper to your {@link android.app.backup.BackupAgentHelper}.</li> +</ol> + +<p>The following sections describe how to create a backup agent using each of the available +helpers.</p> + + + +<h3 id="SharedPreferences">Backing up SharedPreferences</h3> + +<p>When you instantiate a {@link android.app.backup.SharedPreferencesBackupHelper}, you must +include the name of one or more {@link android.content.SharedPreferences} files.</p> + +<p>For example, to back up a {@link android.content.SharedPreferences} file named +"user_preferences", a complete backup agent using {@link android.app.backup.BackupAgentHelper} looks +like this:</p> + +<pre> +public class MyPrefsBackupAgent extends BackupAgentHelper { + // The name of the SharedPreferences file + static final String PREFS = "user_preferences"; + + // A key to uniquely identify the set of backup data + static final String PREFS_BACKUP_KEY = "prefs"; + + // Allocate a helper and add it to the backup agent + @Override + public void onCreate() { + SharedPreferencesBackupHelper helper = + new SharedPreferencesBackupHelper(this, PREFS); + addHelper(PREFS_BACKUP_KEY, helper); + } +} +</pre> + +<p>That's it! That's your entire backup agent. The {@link +android.app.backup.SharedPreferencesBackupHelper} includes all the code +needed to backup and restore a {@link android.content.SharedPreferences} file.</p> + +<p>When the Backup Manager calls {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} and {@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) +onRestore()}, {@link android.app.backup.BackupAgentHelper} calls your backup helpers to perform +backup and restore for your specified files.</p> + +<p class="note"><strong>Note:</strong> The methods of {@link android.content.SharedPreferences} +are threadsafe, so +you can safely read and write the shared preferences file from your backup agent and +other activities.</p> + + + +<h3 id="Files">Backing up other files</h3> + +<p>When you instantiate a {@link android.app.backup.FileBackupHelper}, you must include the name of +one or more files that are saved to your application's <a +href="{@docRoot}guide/topics/data/data-storage.html#filesInternal">internal storage</a> +(as specified by {@link android.content.ContextWrapper#getFilesDir()}, which is the same +location where {@link android.content.Context#openFileOutput(String,int) openFileOutput()} writes +files).</p> + +<p>For example, to backup two files named "scores" and "stats," a backup agent using {@link +android.app.backup.BackupAgentHelper} looks like this:</p> + +<pre> +public class MyFileBackupAgent extends BackupAgentHelper { + // The name of the file + static final String TOP_SCORES = "scores"; + static final String PLAYER_STATS = "stats"; + + // A key to uniquely identify the set of backup data + static final String FILES_BACKUP_KEY = "myfiles"; + + // Allocate a helper and add it to the backup agent + @Override + public void onCreate() { + FileBackupHelper helper = new FileBackupHelper(this, + TOP_SCORES, PLAYER_STATS); + addHelper(FILES_BACKUP_KEY, helper); + } +} +</pre> + +<p>The {@link android.app.backup.FileBackupHelper} includes all the code necessary to backup and +restore files that are saved to your application's <a +href="{@docRoot}guide/topics/data/data-storage.html#filesInternal">internal storage</a>..</p> + +<p>However, reading and writing to files on internal storage is <strong>not threadsafe</strong>. To +ensure that your backup agent does not read or write your files at the same time as your activities, +you must use synchronized statements each time you perform a read or write. For example, +in any Activity where you read and write the file, you need an object to use as the intrinsic +lock for the synchronized statements:</p> + +<pre> +// Object for intrinsic lock +static final Object sDataLock = new Object(); +</pre> + +<p>Then create a synchronized statement with this lock each time you read or write the files. For +example, here's a synchronized statement for writing the latest score in a game to a file:</p> + +<pre> +try { + synchronized (MyActivity.sDataLock) { + File dataFile = new File({@link android.content.Context#getFilesDir()}, TOP_SCORES); + RandomAccessFile raFile = new RandomAccessFile(dataFile, "rw"); + raFile.writeInt(score); + } +} catch (IOException e) { + Log.e(TAG, "Unable to write to file"); +} +</pre> + +<p>You should synchronize your read statements with the same lock.</p> + +<p>Then, in your {@link android.app.backup.BackupAgentHelper}, you must override {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} and {@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) +onRestore()} to synchronize the backup and restore operations with the same +intrinsic lock. For example, the {@code MyFileBackupAgent} example from above needs the following +methods:</p> + +<pre> +@Override +public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) throws IOException { + // Hold the lock while the FileBackupHelper performs backup + synchronized (MyActivity.sDataLock) { + super.onBackup(oldState, data, newState); + } +} + +@Override +public void onRestore(BackupDataInput data, int appVersionCode, + ParcelFileDescriptor newState) throws IOException { + // Hold the lock while the FileBackupHelper restores the file + synchronized (MyActivity.sDataLock) { + super.onRestore(data, appVersionCode, newState); + } +} +</pre> + +<p>That's it. All you need to do is add your {@link android.app.backup.FileBackupHelper} in the +{@link android.app.backup.BackupAgent#onCreate()} method and override {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} and {@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) +onRestore()} to synchronize read and write operations.</p> + +<div class="special"> +<p>For an example implementation of {@link +android.app.backup.BackupAgentHelper} with {@link android.app.backup.FileBackupHelper}, see the +{@code FileHelperExampleAgent} class in the <a +href="{@docRoot}resources/samples/BackupRestore/index.html">Backup and Restore</a> sample +application.</p> +</div> + + + + + + +<h2 id="RestoreVersion">Checking the restore data version</h2> + +<p>When the Backup Manager saves your data to cloud storage, it automatically includes the version +of your application, as defined by your manifest file's <a +href="{@docRoot}guide/topics/manifest/manifest-element.html#vcode">{@code android:versionCode}</a> +attribute. Before the Backup Manager calls your backup agent to restore your data, it +looks at the <a +href="{@docRoot}guide/topics/manifest/manifest-element.html#vcode">{@code +android:versionCode}</a> of the installed application and compares it to the value +recorded in the restore data set. If the version recorded in the restore data set is +<em>newer</em> than the application version on the device, then the user has downgraded their +application. In this case, the Backup Manager will abort the restore operation for your application +and not call your {@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) onRestore()} +method, because the restore set is considered meaningless to an older version.</p> + +<p>You can override this behavior with the <a +href="{@docRoot}guide/topics/manifest/application-element.html#restoreany">{@code +android:restoreAnyVersion}</a> attribute. This attribute is either "{@code true}" or "{@code +false}" to indicate whether you want to restore the application regardless of the restore set +version. The default value is "{@code false}". If you define this to be "{@code true}" then the +Backup Manager will ignore the <a +href="{@docRoot}guide/topics/manifest/manifest-element.html#vcode">{@code android:versionCode}</a> +and call your {@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) onRestore()} +method in all cases. In doing so, you can manually check for the version difference in your {@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) onRestore()} +method and take any steps necessary to make the data compatible if the versions conflict.</p> + +<p>To help you handle different versions during a restore operation, the {@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) onRestore()} +method passes you the version code included with the restore data set as the {@code appVersionCode} +parameter. You can then query the current application's version code with the {@link +android.content.pm.PackageInfo#versionCode PackageInfo.versionCode} field. For example:</p> + +<pre> +PackageInfo info; +try { + String name = {@link android.content.ContextWrapper#getPackageName() getPackageName}(); + info = {@link android.content.ContextWrapper#getPackageManager +getPackageManager}().{@link android.content.pm.PackageManager#getPackageInfo(String,int) +getPackageInfo}(name,0); +} catch (NameNotFoundException nnfe) { + info = null; +} + +int version; +if (info != null) { + version = info.versionCode; +} +</pre> + +<p>Then simply compare the {@code version} acquired from {@link android.content.pm.PackageInfo} +to the {@code appVersionCode} passed into {@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) onRestore()}. +</p> + +<p class="caution"><strong>Caution:</strong> Be certain you understand the consequences of setting +<a href="{@docRoot}guide/topics/manifest/application-element.html#restoreany">{@code +android:restoreAnyVersion}</a> to "{@code true}" for your application. If each version of your +application that supports backup does not properly account for variations in your data format during +{@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) onRestore()}, +then the data on the device could be saved in a format incompatible with the version currently +installed on the device.</p> + + + +<h2 id="RequestingBackup">Requesting backup</h2> + +<p>You can request a backup operation at any time by calling {@link +android.app.backup.BackupManager#dataChanged()}. This method notifies the Backup Manager that you'd +like to backup your data using your backup agent. The Backup Manager then calls your backup +agent's {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()} method at an opportune time in the future. Typically, you should +request a backup each time your data changes (such as when the user changes an application +preference that you'd like to back up). If you call {@link +android.app.backup.BackupManager#dataChanged()} several times consecutively, before the Backup +Manager requests a backup from your agent, your agent still receives just one call to {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor) +onBackup()}.</p> + +<p class="note"><strong>Note:</strong> While developing your application, you can request a +backup and initiate an immediate backup operation with the <a +href="{@docRoot}tools/help/bmgr.html">{@code bmgr} +tool</a>.</p> + + +<h2 id="RequestingRestore">Requesting restore</h2> + +<p>During the normal life of your application, you shouldn't need to request a restore operation. +They system automatically checks for backup data and performs a restore when your application is +installed. However, you can manually request a restore operation by calling {@link +android.app.backup.BackupManager#requestRestore(RestoreObserver) requestRestore()}, if necessary. In +which case, the Backup Manager calls your {@link +android.app.backup.BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor) onRestore()} +implementation, passing the data from the current set of backup data.</p> + +<p class="note"><strong>Note:</strong> While developing your application, you can request a +restore operation with the <a href="{@docRoot}tools/help/bmgr.html">{@code bmgr} +tool</a>.</p> + + +<h2 id="Migrating">Migrating to Auto Backup</h2> +<p>You can transition your app to full-data backups by setting <a href="{@docRoot}reference/android/R.attr.html#fullBackupOnly">android:fullBackupOnly</a> to <code>true</code> in the <code><application></code> element in the manifest file. When +running on a device with Android 5.1 (API level 22) or lower, your app ignores +this value in the manifest, and continues performing Key/Value Backups. When +running on a device with Android 6.0 (API level 23) or higher, your app performs +Auto Backup instead of Key/Value Backup.
\ No newline at end of file diff --git a/docs/html/guide/topics/data/testingbackup.jd b/docs/html/guide/topics/data/testingbackup.jd new file mode 100644 index 000000000000..6ff5837e2411 --- /dev/null +++ b/docs/html/guide/topics/data/testingbackup.jd @@ -0,0 +1,181 @@ +page.title=Testing Backup and Restore +page.tags=backup, marshmallow, androidm +page.keywords=backup, restore, testing + +@jd:body + +<div id="qv-wrapper"> +<div id="qv"> + <h2>In this document</h2> + <ol> + <li><a href="#HowBackupWorks">How backup works</a></li> + <li><a href="#Prerequisites">Prerequisites</a></li> + <li><a href="#Preparing">Preparing your device or emulator</a></li> + <li><a href="#TestingBackup">Testing backup</a></li> + <li><a href="#TestingRestore">Testing restore</a></li> + <li><a href="#Troubleshooting">Troubleshooting</a></li> + </ol> + +</div> +</div> + +<p>This page shows you how to manually trigger Auto Backup, Key/Value Backup, and restore operations to +ensure your app saves and restores data properly. + +<h2 id="HowBackupWorkso">How backup works</h2> +<p>The section describes various pieces in the Android backup framework and how they +interact with apps that support Auto Backup and Key/Value Backup. During the app +development phase, most of the inner working of the framework were abstracted away, so you didn't need to know this information. However, during the +testing phase, an understanding of these concepts is important. + +<p>The following diagram illustrates how data flows during backup and restore: + +<img src="images/backup-framework.png" alt="backup-framework"> + +<p>The <em>Backup Manager Service</em> is an Android system +service which orchestrates and initiates backup and restore operations. The service +is accessible through the {@link android.app.backup.BackupManager} +API. During a backup operation, the service queries your app for backup data, +then hands it to the <em>backup transport</em>, which then archives the data. +During a restore operation, the backup manager service retrieves the backup data +from the backup transport and restores the data to the device. + +<p><em>Backup Transports</em> are Android components that are responsible +for storing and retrieving backups. An Android device can have zero or more +backup transports, but only one of those transports can be marked active. The +available backup transports may differ from device to device (due to +customizations by device manufacturers and service providers), but most Google +Play enabled devices ship with the following transports: +</p><ul> +<li><strong>Google GMS Transport</strong>(default) - the active backup +transport on most devices, part of <a href="https://www.android.com/gms/">Google Mobile Services</a>. This documentation assumes that users are using the +Google GMS transport. This transport stores Auto Backup data in a private folder in the +user's Google Drive account. Key/Value Backup data is stored in the <a href="{@docRoot}google/backup/index.html">Android Backup Service</a>. +<li><strong>Local Transport</strong> - stores backup data locally on the device. +This transport is typically used for development/debugging purposes and is not +useful in the real world.</li></ul> + +<p>If a device does not have any backup transports, then the data cannot be +backed up. Your app is not adversely affected. + +<p class="caution"><strong>Caution:</strong> Because the backup transport +can differ from device to device, Android cannot guarantee the security +of your data while using backup. Be cautious about using backup to store +sensitive data, such as usernames and passwords. + +<h2 id="Prerequisites">Prerequisites</h2> +<p>You need to know a bit about the following tools: + +<ul> + <li><a href="{@docRoot}studio/command-line/adb.html">adb</a> +- to run commands on the device or emulator + <li><a href="{@docRoot}studio/command-line/bmgr.html">bmgr</a> - to +perform various backup and restore operations + <li><a href="{@docRoot}studio/command-line/logcat.html">logcat</a> +- to see the output of backup and restore operations.</li></ul> + +<h2 id="Preparing">Preparing your device or +emulator</h2> +<p>Prepare your device or emulator for backup testing by working through the +following checklist: +<ul> + <li>For Auto Backup, check that you are using a device or emulator running +Android 6.0 (API level 23) or higher.</li> + <li>For Key/Value Backup, check that you are using a device or emulator running +Android 2.2 (API level 8) or higher.</li> + <li>Check that backup and restore is enabled on the device or emulator. There +are two ways to check: <ul> + <li>On the device, go to <strong>Settings -> Backup & Restore</strong>. + <li>From adb shell, run <code>bmgr enable</code></li></ul> + <p>On physical devices, backup and restore is typically enabled during the +initial setup wizard. Emulators do not run the setup wizard, so don't forget to +enable backup and specify a backup account in device settings. + <li>Make sure the GMS Backup Transport is available and active by running the +command from adb shell: + <pre class="prettyprint">$ bmgr list transports</pre> + <p>Then, check the console for the following output: + <pre class="prettyprint">android/com.android.internal.backup.LocalTransport +* com.google.android.gms/.backup.BackupTransportService</pre> + <p>Physical devices without Google Play and emulators without Google APIs +might not include the GMS Backup Transport. This article assumes you are using +the GMS Backup Transport. You can test backup and restore with other backup +transports, but the procedure and output can differ. +</ul> + +<h2 id="TestingBackup">Testing backup</h2> +<p>To initiate a backup of your app, run the following command: + +<pre class="prettyprint">$ bmgr backupnow <PACKAGE></pre> + +<p>The <code>backupnow</code> command runs either a Key/Value Backup or Auto Backup depending on +the package's manifest declarations. Check logcat to see the output of the +backup procedure. For example: + +<pre class="prettyprint">D/BackupManagerService: fullTransportBackup() +I/GmsBackupTransport: Attempt to do full backup on <PACKAGE> + +---- or ---- + +V/BackupManagerService: Scheduling immediate backup pass +D/PerformBackupTask: starting key/value Backup of BackupRequest{pkg=<PACKAGE>} +</pre> + +<p>If the <code>backupnow</code> command is not available on your device, you need to run one +of the following commands: +<ul> + <li>For Auto Backups, run: <code>bmgr fullbackup <PACKAGE></code> + <li>For Key/Value Backups, schedule and run your backup with the following +commands: + <pre class="prettyprint">$ bmgr backup <PACKAGE> +$ bmgr run</pre> + <p><code>bmgr backup</code> adds your app to the Backup Manager's queue. <code>bmgr run</code> initiates the +backup operation, which forces the Backup Manager to perform all backup requests +that are in its queue. + </li></ul> + +<h2 id="TestingRestore">Testing restore</h2> +<p>To manually initiate a restore, run the following command: + +<pre class="prettyprint">$ bmgr restore <PACKAGE></pre> + +<p>Warning: This action stops your app and wipes its data before performing the +restore operation. + +<p>Then, check logcat to see the output of the restore procedure. For example: + +<pre class="prettyprint">V/BackupManagerService: beginRestoreSession: pkg=<PACKAGE> transport=null +V/RestoreSession: restorePackage pkg=<PACKAGE> token=368abb4465c5c683 +... +I/BackupManagerService: Restore complete. +</pre> + +<p>You also can test automatic restore for your app by uninstalling and +reinstalling your app either with <code>adb</code> or through the Google +Play Store app. + +<h2 id="Troubleshooting">Troubleshooting</h2> +<p><strong>Exceeded Quota</strong> + +<p>If you see the the following messages in logcat: + +<pre class="prettyprint">I/PFTBT: Transport rejected backup of <PACKAGE>, skipping + +--- or --- + +I/PFTBT: Transport quota exceeded for package: <PACKAGE> +</pre> + +<p>Your app has exceeded the quota and has been banned from backing up +data on this device. To lift the ban, either factory reset your device or change +the backup account. + +<p><strong>Full Backup Not Possible</strong> + +<p>If you see the the following message in logcat: + +<pre class="prettyprint">I/BackupManagerService: Full backup not currently possible -- key/value backup not yet run? +</pre> + +<p>The fullbackup operation failed because no Key/Value Backup operation has yet +occurred on the device. Trigger a Key/Value Backup with the command <code>bmgr +run </code>and then try again.
\ No newline at end of file diff --git a/docs/html/guide/topics/manifest/provider-element.jd b/docs/html/guide/topics/manifest/provider-element.jd index 1947849e56f8..0e729c36f37e 100644 --- a/docs/html/guide/topics/manifest/provider-element.jd +++ b/docs/html/guide/topics/manifest/provider-element.jd @@ -215,17 +215,15 @@ it can also be set as a raw string. </p></dd> <dt><a name="multi"></a>{@code android:multiprocess}</dt> -<dd>Whether or not an instance of the content provider can be created in -every client process — "{@code true}" if instances can run in multiple -processes, and "{@code false}" if not. The default value is "{@code false}". - -<p> -Normally, a content provider is instantiated in the process of the -application that defined it. However, if this flag is set to "{@code true}", -the system can create an instance in every process where there's a client -that wants to interact with it, thus avoiding the overhead of interprocess -communication. -</p></dd> +<dd>If the app runs in multiple processes, this attribute determines whether +multiple instances of the content provder are created. If <code>true</code>, +each of the app's processes has its own content provider object. If +<code>false</code>, the app's processes share only one content provider object. +The default value is <code>false</code>. + +<p>Setting this flag to <code>true</code> may improve performance by reducing +the overhead of interprocess communication, but it also increases the memory +footprint of each process.</p></dd> <dt><a name="nm"></a>{@code android:name}</dt> <dd>The name of the class that implements the content provider, a subclass of diff --git a/docs/html/guide/topics/resources/drawable-resource.jd b/docs/html/guide/topics/resources/drawable-resource.jd index aae0cbae99b3..4587ae4ec5db 100644 --- a/docs/html/guide/topics/resources/drawable-resource.jd +++ b/docs/html/guide/topics/resources/drawable-resource.jd @@ -1270,7 +1270,7 @@ the right edge, a right gravity clips the left edge, and neither clips both edge progressively reveal the image:</p> <pre> ImageView imageview = (ImageView) findViewById(R.id.image); -ClipDrawable drawable = (ClipDrawable) imageview.getDrawable(); +ClipDrawable drawable = (ClipDrawable) imageview.getBackground(); drawable.setLevel(drawable.getLevel() + 1000); </pre> diff --git a/docs/html/guide/topics/ui/accessibility/services.jd b/docs/html/guide/topics/ui/accessibility/services.jd index c6db855adba4..dbc69ef884ee 100644 --- a/docs/html/guide/topics/ui/accessibility/services.jd +++ b/docs/html/guide/topics/ui/accessibility/services.jd @@ -79,22 +79,15 @@ must also request the {@link android.Manifest.permission#BIND_ACCESSIBILITY_SERV as shown in the following sample:</p> <pre> -<manifest> - ... - <uses-permission ... /> - ... <application> - ... <service android:name=".MyAccessibilityService" - android:label="@string/accessibility_service_label" - android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> + android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" + android:label="@string/accessibility_service_label"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> </service> - <uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" /> </application> -</manifest> </pre> <p>These declarations are required for all accessibility services deployed on Android 1.6 (API Level diff --git a/docs/html/guide/topics/ui/drag-drop.jd b/docs/html/guide/topics/ui/drag-drop.jd index f486c8a62544..d6e07a56f60d 100644 --- a/docs/html/guide/topics/ui/drag-drop.jd +++ b/docs/html/guide/topics/ui/drag-drop.jd @@ -321,7 +321,7 @@ DraggableDot.java</a> in <a href="{@docRoot}resources/samples/ApiDemos/index.htm <p> If the listener wants to continue receiving drag events for this operation, it must return boolean <code>true</code> to the system. - <\p> + </p> </td> </tr> <tr> diff --git a/docs/html/guide/topics/ui/multi-window.jd b/docs/html/guide/topics/ui/multi-window.jd index dede557ecc9a..bab582dd0b8c 100644 --- a/docs/html/guide/topics/ui/multi-window.jd +++ b/docs/html/guide/topics/ui/multi-window.jd @@ -215,7 +215,7 @@ android:resizeableActivity=["true" | "false"] Set this attribute in your manifest's <a href= "{@docRoot}guide/topics/manifest/activity-element"><code><activity></code></a> node to indicate whether the activity supports <a href= - "{@docRoot}training/tv/playback/picture-in-picture.jd">Picture-in-Picture</a> + "{@docRoot}training/tv/playback/picture-in-picture.html">Picture-in-Picture</a> display. This attribute is ignored if <code>android:resizeableActivity</code> is false. </p> diff --git a/docs/html/topic/performance/_book.yaml b/docs/html/topic/performance/_book.yaml index e053a2c1bdfe..4021e85c14aa 100644 --- a/docs/html/topic/performance/_book.yaml +++ b/docs/html/topic/performance/_book.yaml @@ -1,34 +1,61 @@ toc: -- title: Reducing Network Battery Drain - path: /topic/performance/power/network/index.html +- title: Optimizing for Battery Life + path: /topic/performance/power/index.html path_attributes: - name: description - value: Access the network while going easy on battery life. + value: Learn to make your app more battery-friendly. section: - - title: Collecting Network Traffic Data - path: /topic/performance/power/network/gather-data.html - - title: Analyzing Network Traffic Data - path: /topic/performance/power/network/analyze-data.html - - title: Optimizing User-Initiated Network Use - path: /topic/performance/power/network/action-user-traffic.html - - title: Optimizing Server-Initiated Network Use - path: /topic/performance/power/network/action-server-traffic.html - - title: Optimizing General Network Use - path: /topic/performance/power/network/action-any-traffic.html -- title: Implementing Doze - path: /training/monitoring-device-state/doze-standby.html + - title: Network Use and Battery Consumption + path: /topic/performance/power/network/index.html + section: + - title: Collecting Network Traffic Data + path: /topic/performance/power/network/gather-data.html + - title: Analyzing Data Traffic + path: /topic/performance/power/network/analyze-data.html + - title: Optimizing User-Initiated Network Use + path: /topic/performance/power/network/action-user-traffic.html + - title: Optimizing App-Initiated Network Use + path: topic/performance/power/network/action-app-traffic.html + - title: Optimizing Server-Initiated Network Use + path: /topic/performance/power/network/action-server-traffic.html + - title: Optimizing General Network Use + path: /topic/performance/power/network/action-any-traffic.html + - title: Doze and App Standby + path: /training/monitoring-device-state/doze-standby.html + path_attributes: + - name: description + value: Help ensure the device isn't depleting the battery when not in use. + - title: Battery Historian + path: /topic/performance/power/battery-historian.html +- title: Rendering + path: /topic/performance/rendering/index.html path_attributes: - name: description - value: Help ensure the device isn't depleting the battery when not in use. -- title: Launch-Time Performance - path: /topic/performance/launch-time.html -- title: Better Performance through Threading - path: /topic/performance/threads.html -- title: Optimizing View Hierarchies - path: /topic/performance/optimizing-view-hierarchies.html -- title: Background Optimization - path: /topic/performance/background-optimization.html + value: Speed up your app's rendering + section: + - title: Reducing Overdraw + path: /topic/performance/rendering/overdraw.html + - title: Performance and View Hierarchies + path: /topic/performance/rendering/optimizing-view-hierarchies.html + - title: Analyzing with Profile GPU Rendering + path: /topic/performance/rendering/profile-gpu.html - title: Intelligent Job-Scheduling path: /topic/performance/scheduling.html +- title: Background Optimization + path: /topic/performance/background-optimization.html - title: Reducing APK Size path: /topic/performance/reduce-apk-size.html +- title: Reducing Image Download Sizes + path: /topic/performance/network-xfer.html +- title: Launch-Time Performance + path: /topic/performance/launch-time.html +- title: Better Performance through Threading + path: /topic/performance/threads.html +- title: Manage Your App's Memory + path: /topic/performance/memory.html +- title: Overview of Memory Managemement + path: /topic/performance/memory-overview.html + path_attributes: + - name: description + value: How to keep your app's memory footprint small in order to improve performance on a variety of mobile devices. + diff --git a/docs/html/topic/performance/images/app-rankings.png b/docs/html/topic/performance/images/app-rankings.png Binary files differnew file mode 100644 index 000000000000..9dd60e54735f --- /dev/null +++ b/docs/html/topic/performance/images/app-rankings.png diff --git a/docs/html/topic/performance/images/bars.png b/docs/html/topic/performance/images/bars.png Binary files differnew file mode 100644 index 000000000000..3afea465c975 --- /dev/null +++ b/docs/html/topic/performance/images/bars.png diff --git a/docs/html/topic/performance/images/beforeafterindexed.png b/docs/html/topic/performance/images/beforeafterindexed.png Binary files differnew file mode 100644 index 000000000000..dc7762ed42a4 --- /dev/null +++ b/docs/html/topic/performance/images/beforeafterindexed.png diff --git a/docs/html/topic/performance/images/comparison.png b/docs/html/topic/performance/images/comparison.png Binary files differnew file mode 100644 index 000000000000..18f204c31d1b --- /dev/null +++ b/docs/html/topic/performance/images/comparison.png diff --git a/docs/html/topic/performance/images/decisions.png b/docs/html/topic/performance/images/decisions.png Binary files differnew file mode 100644 index 000000000000..d4f21f8f770b --- /dev/null +++ b/docs/html/topic/performance/images/decisions.png diff --git a/docs/html/topic/performance/images/dropdown.png b/docs/html/topic/performance/images/dropdown.png Binary files differnew file mode 100644 index 000000000000..59ec6110e5ef --- /dev/null +++ b/docs/html/topic/performance/images/dropdown.png diff --git a/docs/html/topic/performance/images/generic-timeline.png b/docs/html/topic/performance/images/generic-timeline.png Binary files differnew file mode 100644 index 000000000000..04388b6f4abb --- /dev/null +++ b/docs/html/topic/performance/images/generic-timeline.png diff --git a/docs/html/topic/performance/images/moarparrots.png b/docs/html/topic/performance/images/moarparrots.png Binary files differnew file mode 100644 index 000000000000..ee10ec158936 --- /dev/null +++ b/docs/html/topic/performance/images/moarparrots.png diff --git a/docs/html/topic/performance/images/palette.png b/docs/html/topic/performance/images/palette.png Binary files differnew file mode 100644 index 000000000000..eb6be6b4246c --- /dev/null +++ b/docs/html/topic/performance/images/palette.png diff --git a/docs/html/topic/performance/images/parrot.png b/docs/html/topic/performance/images/parrot.png Binary files differnew file mode 100644 index 000000000000..7a8b86a97b1e --- /dev/null +++ b/docs/html/topic/performance/images/parrot.png diff --git a/docs/html/topic/performance/images/pug-visualization.png b/docs/html/topic/performance/images/pug-visualization.png Binary files differnew file mode 100644 index 000000000000..8270d0e37d1d --- /dev/null +++ b/docs/html/topic/performance/images/pug-visualization.png diff --git a/docs/html/topic/performance/images/pugspecificdata.png b/docs/html/topic/performance/images/pugspecificdata.png Binary files differnew file mode 100644 index 000000000000..1c2be837ba26 --- /dev/null +++ b/docs/html/topic/performance/images/pugspecificdata.png diff --git a/docs/html/topic/performance/images/s-generic-closeup.png b/docs/html/topic/performance/images/s-generic-closeup.png Binary files differnew file mode 100644 index 000000000000..6685d51a32a1 --- /dev/null +++ b/docs/html/topic/performance/images/s-generic-closeup.png diff --git a/docs/html/topic/performance/images/s-profiler-legend.png b/docs/html/topic/performance/images/s-profiler-legend.png Binary files differnew file mode 100644 index 000000000000..968fd381156a --- /dev/null +++ b/docs/html/topic/performance/images/s-profiler-legend.png diff --git a/docs/html/topic/performance/images/vq.gif b/docs/html/topic/performance/images/vq.gif Binary files differnew file mode 100644 index 000000000000..cbf6a3504059 --- /dev/null +++ b/docs/html/topic/performance/images/vq.gif diff --git a/docs/html/topic/performance/index.jd b/docs/html/topic/performance/index.jd index e08db15baa08..2b6b1972a03b 100644 --- a/docs/html/topic/performance/index.jd +++ b/docs/html/topic/performance/index.jd @@ -1,4 +1,4 @@ -page.title=Performance +page.title=Performance and Power page.article=true page.metaDescription=Improve your app's performance by learning how to optimize power consumption, launch times, and other important areas of performance. diff --git a/docs/html/topic/performance/memory-overview.jd b/docs/html/topic/performance/memory-overview.jd new file mode 100644 index 000000000000..58067d2be4b8 --- /dev/null +++ b/docs/html/topic/performance/memory-overview.jd @@ -0,0 +1,288 @@ +page.title=Overview of Android Memory Management +page.tags=ram,memory,paging,mmap + +@jd:body + +<div id="qv-wrapper"> +<div id="qv"> + +<h2>In this document</h2> +<ol class="nolist"> + <li><a href="#gc">Garbage collection</a></li> + <li><a href="#SharingRAM">Sharing Memory</a></li> + <li><a href="#AllocatingRAM">Allocating and Reclaiming App Memory</a></li> + <li><a href="#RestrictingMemory">Restricting App Memory</a></li> + <li><a href="#SwitchingApps">Switching Apps</a></li> +</ol> +<h2>See Also</h2> +<ul> + <li><a href="{@docRoot}training/articles/memory.html">Manage Your App's Memory</a> + </li> + <li><a href="{@docRoot}studio/profile/investigate-ram.html">Investigating Your RAM Usage</a> + </li> +</ul> + +</div> +</div> + +<p> + The Android Runtime (ART) and Dalvik virtual machine use + <a href="http://en.wikipedia.org/wiki/Paging" class="external-link">paging</a> + and <a href="http://en.wikipedia.org/wiki/Memory-mapped_files" class="external-link">memory-mapping</a> + (mmapping) to manage memory. This means that any memory an app + modifies—whether by allocating + new objects or touching mmapped pages—remains resident in RAM and + cannot be paged out. The only way to release memory from an app is to release + object references that the app holds, making the memory available to the + garbage collector. + That is with one exception: any files + mmapped in without modification, such as code, + can be paged out of RAM if the system wants to use that memory elsewhere. +</p> + +<p> + This page explains how Android manages app processes and memory + allocation. For more information about how to manage memory more efficiently + in your app, see + <a href="{@docRoot}training/articles/memory.html">Manage Your App's Memory</a>. +</p> + +<!-- Section 1 #################################################### --> + +<h2 id="gc">Garbage collection</h2> + +<p> + A managed memory environment, like the ART or Dalvik virtual machine, + keeps track of each memory allocation. Once it determines + that a piece of memory is no longer being used by the program, + it frees it back to the heap, without any intervention from the programmer. + The mechanism for reclaiming unused memory + within a managed memory environment + is known as <i>garbage collection</i>. Garbage collection has two goals: + find data objects in a program that cannot be accessed in the future; and + reclaim the resources used by those objects. +</p> + +<p> + Android’s memory heap is a generational one, meaning that there are + different buckets of allocations that it tracks, + based on the expected life and size of an object being allocated. + For example, recently allocated objects belong in the <i>Young generation</i>. + When an object stays active long enough, it can be promoted + to an older generation, followed by a permanent generation. +</p> + +<p> + Each heap generation has its own dedicated upper limit on the amount + of memory that objects there can occupy. Any time a generation starts + to fill up, the system executes a garbage collection + event in an attempt to free up memory. The duration of the garbage collection + depends on which generation of objects it's collecting + and how many active objects are in each generation. +</p> + +<p> + Even though garbage collection can be quite fast, it can still + affect your app's performance. You don’t generally control + when a garbage collection event occurs from within your code. + The system has a running set of criteria for determining when to perform + garbage collection. When the criteria are satisfied, + the system stops executing the process and begins garbage collection. If + garbage collection occurs in the middle of an intensive processing loop + like an animation or during music playback, it can increase processing time. + This increase can potentially push code execution in your app past the + recommended 16ms threshold for efficient and smooth frame rendering. +</p> + +<p> + Additionally, your code flow may perform kinds of work that + force garbage collection events to occur + more often or make them last longer-than-normal. + For example, if you allocate multiple objects in the + innermost part of a for-loop during each frame of an alpha + blending animation, you might pollute your memory heap with a + lot of objects. + In that circumstance, the garbage collector executes multiple garbage + collection events and can degrade the performance of your app. +</p> + +<p> + For more general information about garbage collection, see + <a href="https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)" + class="external-link">Garbage collection</a>. +</p> + +<!-- Section 2 #################################################### --> + +<h2 id="SharingRAM">Sharing Memory</h2> + +<p> + In order to fit everything it needs in RAM, + Android tries to share RAM pages across processes. It + can do so in the following ways: +</p> + +<ul> + <li> + Each app process is forked from an existing process called Zygote. + The Zygote process starts when the system boots and loads common + framework code and resources + (such as activity themes). To start a new app process, + the system forks the Zygote process then + loads and runs the app's code in the new process. + This approach allows most of the RAM pages allocated for + framework code and resources to be shared across all app processes. + </li> + + <li> + Most static data is mmapped into a process. + This technique allows data to be shared + between processes, and also allows it to be paged + out when needed. Example static data include: + Dalvik code (by placing it in a pre-linked <code>.odex</code> + file for direct mmapping), app resources + (by designing the resource table to be a structure + that can be mmapped and by aligning the zip + entries of the APK), and traditional project + elements like native code in <code>.so</code> files. + </li> + + <li> + In many places, Android shares the same dynamic + RAM across processes using explicitly allocated + shared memory regions (either with ashmem or gralloc). + For example, window surfaces use shared + memory between the app and screen compositor, and + cursor buffers use shared memory between the + content provider and client. + </li> +</ul> + +<p> + Due to the extensive use of shared memory, determining + how much memory your app is using requires + care. Techniques to properly determine your app's + memory use are discussed in + <a href="{@docRoot}studio/profile/investigate-ram.html">Investigating Your RAM Usage</a>. +</p> + +<!-- Section 3 #################################################### --> + +<h2 id="AllocatingRAM">Allocating and Reclaiming App Memory</h2> + +<p> + The Dalvik heap is constrained to a + single virtual memory range for each app process. This defines + the logical heap size, which can grow as it needs to + but only up to a limit that the system defines + for each app. +</p> + +<p> + The logical size of the heap is not the same as + the amount of physical memory used by the heap. + When inspecting your app's heap, Android computes + a value called the Proportional Set Size (PSS), + which accounts for both dirty and clean pages + that are shared with other processes—but only in an + amount that's proportional to how many apps share + that RAM. This (PSS) total is what the system + considers to be your physical memory footprint. + For more information about PSS, see the + <a href="{@docRoot}studio/profile/investigate-ram.html">Investigating Your RAM Usage</a> + guide. +</p> + +<p> + The Dalvik heap does not compact the logical + size of the heap, meaning that Android does not + defragment the heap to close up space. Android + can only shrink the logical heap size when there + is unused space at the end of the heap. However, + the system can still reduce physical memory used by the heap. + After garbage collection, Dalvik + walks the heap and finds unused pages, then returns + those pages to the kernel using madvise. So, paired + allocations and deallocations of large + chunks should result in reclaiming all (or nearly all) + the physical memory used. However, + reclaiming memory from small allocations can be much + less efficient because the page used + for a small allocation may still be shared with + something else that has not yet been freed. + +</p> + +<!-- Section 4 #################################################### --> + +<h2 id="RestrictingMemory">Restricting App Memory</h2> + +<p> + To maintain a functional multi-tasking environment, + Android sets a hard limit on the heap size + for each app. The exact heap size limit varies + between devices based on how much RAM the device + has available overall. If your app has reached the + heap capacity and tries to allocate more + memory, it can receive an {@link java.lang.OutOfMemoryError}. +</p> + +<p> + In some cases, you might want to query the + system to determine exactly how much heap space you + have available on the current device—for example, to + determine how much data is safe to keep in a + cache. You can query the system for this figure by calling + {@link android.app.ActivityManager#getMemoryClass() }. + This method returns an integer indicating the number of + megabytes available for your app's heap. +</p> + +<!-- Section 5 #################################################### --> + +<h2 id="SwitchingApps">Switching apps</h2> + +<p> + When users switch between apps, + Android keeps apps that + are not foreground—that is, not visible to the user or running a + foreground service like music playback— + in a least-recently used (LRU) cache. + For example, when a user first launches an app, + a process is created for it; but when the user + leaves the app, that process does <em>not</em> quit. + The system keeps the process cached. If + the user later returns to the app, the system reuses the process, thereby + making the app switching faster. +</p> + +<p> + If your app has a cached process and it retains memory + that it currently does not need, + then your app—even while the user is not using it— + affects the system's + overall performance. As the system runs low on memory, + it kills processes in the LRU cache + beginning with the process least recently used. The system also + accounts for processes that hold onto the most memory + and can terminate them to free up RAM. +</p> + +<p class="note"> + <strong>Note:</strong> When the system begins killing processes in the + LRU cache, it primarily works bottom-up. The system also considers which + processes consume more memory and thus provide the system + more memory gain if killed. + The less memory you consume while in the LRU list overall, + the better your chances are + to remain in the list and be able to quickly resume. +</p> + +<p> + For more information about how processes are cached while + not running in the foreground and how + Android decides which ones + can be killed, see the + <a href="{@docRoot}guide/components/processes-and-threads.html">Processes and Threads</a> + guide. +</p> diff --git a/docs/html/topic/performance/memory.jd b/docs/html/topic/performance/memory.jd new file mode 100644 index 000000000000..ef1c4ae0aeea --- /dev/null +++ b/docs/html/topic/performance/memory.jd @@ -0,0 +1,593 @@ +page.title=Manage Your App's Memory +page.tags=ram,low memory,OutOfMemoryError,onTrimMemory + +@jd:body + +<div id="qv-wrapper"> +<div id="qv"> + +<h2>In this document</h2> +<ol> + <li><a href="#monitor">Monitor Available Memory and Memory Usage</a> + <ul> + <li><a href="#AnalyzeRam">Tools for analyzing RAM usage</a></li> + <li><a href="#release">Release memory in response to events</a></li> + <li><a href="#CheckHowMuchMemory">Check how much memory you should use</a></li> + </ul> + </li> + <li><a href="#code">Use More Efficient Code Constructs</a> + <ul> + <li><a href="#Services">Use services sparingly</a></li> + <li><a href="#DataContainers">Use optimized data containers</a></li> + <li><a href="#Abstractions">Be careful with code abstractions</a></li> + <li><a href="#NanoProto">Use nano protobufs for serialized data</a></li> + <li><a href="#churn">Avoid memory churn</a></li> + </ul> + </li> + <li><a href="#remove">Remove Memory-Intensive Resources and Libraries</a> + <ul> + <li><a href="#reduce">Reduce overall APK size</a></li> + <li><a href="#DependencyInjection">Avoid dependency injection frameworks</a></li> + <li><a href="#ExternalLibs">Be careful about using external libraries</a></li> + </ul> + </li> +</ol> +<h2>See Also</h2> +<ul> + <li><a href="{@docRoot}training/articles/memory-overview.html">Overview of Android Memory Management</a> + </li> + <li><a href="{@docRoot}studio/profile/investigate-ram.html">Investigating Your RAM Usage</a> + </li> + <li><a href="{@docRoot}topic/performance/reduce-apk-size.html">Reduce APK Size</a></li> +</ul> + +</div> +</div> + +<!-- INTRO #################################################### --> + +<p> + Random-access memory (RAM) is a valuable + resource in any software development environment, but + it's even more valuable on a mobile operating system + where physical memory is often constrained. + Although both the Android Runtime (ART) and Dalvik virtual machine perform + routine garbage collection, this does not mean you can ignore + when and where your app allocates and releases memory. + You still need to avoid + introducing memory leaks, usually caused by holding onto + object references in static member variables, and + release any {@link java.lang.ref.Reference} objects at the appropriate + time as defined by + lifecycle callbacks. +</p> + +<p> + This page explains how you can + proactively reduce memory usage within your app. + For more information about general + practices to clean up your resources when programming in Java, + refer to other books or online + documentation about managing resource references. + If you’re looking for information about how to + analyze memory in a running app, read + <a href="#AnalyzeRam">Tools for analyzing RAM usage</a>. + For more detailed information about how the Android Runtime and Dalvik + virtual machine manage memory, see the + <a href="{@docRoot}training/articles/memory-overview.html">Overview of Android Memory Management</a>. +</p> + +<!-- Section 1 #################################################### --> + +<h2 id="monitor">Monitor Available Memory and Memory Usage</h2> + +<p> + The Android framework, Android Studio, and Android SDK + can help you analyze and adjust your app's memory usage. + The Android framework + exposes several APIs that allow your app to reduce its memory usage + dynamically during runtime. Android Studio and the Android SDK + contain several tools that allow you to investigate how your + app uses memory. +</p> + +<!-- Section 1.1 #################################################### --> + +<h3 id="AnalyzeRam">Tools for analyzing RAM usage</h3> + +<p> + Before you can fix the memory usage problems in your app, you first need + to find them. Android Studio and the Android SDK include several tools + for analyzing memory usage in your app: +</p> + +<ol> + <li> + The Device Monitor has a Dalvik Debug Monitor Server (DDMS) tool that allows + you to inspect memory allocation within your app process. + You can use this information to understand how your + app uses memory overall. For example, you can force a garbage collection + event and then view the types of objects that remain in memory. You can + use this information to identify operations or actions within your app + that allocate or leave excessive amounts of objects in memory. + + <p>For more information about how to use the DDMS tool, see + <a href="/studio/profile/ddms.html">Using DDMS</a>. + </p> + </li> + + <li> + The Memory Monitor in Android Studio shows you how your app allocates + memory over the course of a single session. + The tool shows a graph of available + and allocated Java memory over time, including garbage collection events. + You can also initiate garbage collection events and take a snapshot of + the Java heap while your app runs. The output from the Memory Monitor tool + can help you identify points when your app experiences excessive garbage + collection events, leading to app slowness. + <p> + For more information about how to use Memory Monitor tool, see + <a href="{@docRoot}tools/debugging/debugging-memory.html#ViewHeap">Viewing Heap Updates</a>. + </p> + </li> + + <li> + Garbage collection events also show up in the Traceview viewer. Traceview + allows you to view trace log files as both a timeline and as a profile + of what happened within a method. You can use this tool to determine + what code was executing when a garbage collection event occurred. + <p> + For more information about how to use the Traceview viewer, see + <a href="https://developer.android.com/studio/profile/traceview.html">Profiling with Traceview and dmtracedump</a>. + </p> + </li> + + <li> + The Allocation Tracker tool in Android Studio gives you a detailed look + at how your app allocates memory. + The Allocation Tracker records an app's memory allocations and lists + all allocated objects within the profiling snapshot. You can use this + tool to track down parts of your code that allocate too many objects. + + <p> + For more information about how to use the Allocation Tracker tool, see + <a href="{docRoot}studio/profile/allocation-tracker-walkthru.html">Allocation Tracker Walkthrough</a>. + </p> + </li> + +</ol> + +<!-- Section 1.2 #################################################### --> + +<h3 id="release">Release memory in response to events</h3> + +<p> + An Android device can run with varying amounts of free memory + depending on the physical amount of RAM on the device and how the user + operates it. The system broadcasts signals to indicate when it is under + memory pressure, and apps should listen for these signals and adjust + their memory usage as appropriate. +</p> + +</p> + You can use the {@link android.content.ComponentCallbacks2} API + to listen for these signals and then adjust your memory + usage in response to app lifecycle + or device events. The + {@link android.content.ComponentCallbacks2#onTrimMemory onTrimMemory()} + method allows your app to listen for memory related events when the app runs + in the foreground (is visible) and when it runs in the background. +</p> + +<p> + To listen for these events, implement the {@link + android.content.ComponentCallbacks2#onTrimMemory onTrimMemory()} + callback in your {@link android.app.Activity} + classes, as shown in the following code snippet. +</p> + +<pre class="prettyprint"> +import android.content.ComponentCallbacks2; +// Other import statements ... + +public class MainActivity extends AppCompatActivity + implements ComponentCallbacks2 { + + // Other activity code ... + + /** + * Release memory when the UI becomes hidden or when system resources become low. + * @param level the memory-related event that was raised. + */ + public void onTrimMemory(int level) { + + // Determine which lifecycle or system event was raised. + switch (level) { + + case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN: + + /* + Release any UI objects that currently hold memory. + + The user interface has moved to the background. + */ + + break; + + case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE: + case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW: + case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL: + + /* + Release any memory that your app doesn't need to run. + + The device is running low on memory while the app is running. + The event raised indicates the severity of the memory-related event. + If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will + begin killing background processes. + */ + + break; + + case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND: + case ComponentCallbacks2.TRIM_MEMORY_MODERATE: + case ComponentCallbacks2.TRIM_MEMORY_COMPLETE: + + /* + Release as much memory as the process can. + + The app is on the LRU list and the system is running low on memory. + The event raised indicates where the app sits within the LRU list. + If the event is TRIM_MEMORY_COMPLETE, the process will be one of + the first to be terminated. + */ + + break; + + default: + /* + Release any non-critical data structures. + + The app received an unrecognized memory level value + from the system. Treat this as a generic low-memory message. + */ + break; + } + } +} +</pre> + +<p> + The + {@link android.content.ComponentCallbacks2#onTrimMemory onTrimMemory()} + callback was added in Android 4.0 (API level 14). For earlier versions, + you can use the + {@link android.content.ComponentCallbacks#onLowMemory()} + callback as a fallback for older versions, which is roughly equivalent to the + {@link android.content.ComponentCallbacks2#TRIM_MEMORY_COMPLETE} event. +</p> + +<!-- Section 1.3 #################################################### --> + +<h3 id="CheckHowMuchMemory">Check how much memory you should use</h3> + +<p> + To allow multiple running processes, Android sets a hard limit + on the heap size alloted for each app. The exact heap size limit varies + between devices based on how much RAM the device + has available overall. If your app has reached the heap capacity and + tries to allocate more + memory, the system throws an {@link java.lang.OutOfMemoryError}. +</p> + +<p> + To avoid running out of memory, you can to query the system to determine + how much heap space you have available on the current device. + You can query the system for this figure by calling + {@link android.app.ActivityManager#getMemoryInfo(android.app.ActivityManager.MemoryInfo) getMemoryInfo()}. + This returns an + {@link android.app.ActivityManager.MemoryInfo } object that provides + information about the device's + current memory status, including available memory, total memory, and + the memory threshold—the memory level below which the system begins + to kill processes. The + {@link android.app.ActivityManager.MemoryInfo } class also exposes a simple + boolean field, + {@link android.app.ActivityManager.MemoryInfo#lowMemory } + that tells you whether the device is running low on memory. +</p> + +<p> + The following code snippet shows an example of how you can use the + {@link android.app.ActivityManager#getMemoryInfo(android.app.ActivityManager.MemoryInfo) getMemoryInfo()}. + method in your application. +</p> + +<pre class="prettyprint"> +public void doSomethingMemoryIntensive() { + + // Before doing something that requires a lot of memory, + // check to see whether the device is in a low memory state. + ActivityManager.MemoryInfo memoryInfo = getAvailableMemory(); + + if (!memoryInfo.lowMemory) { + // Do memory intensive work ... + } +} + +// Get a MemoryInfo object for the device's current memory status. +private ActivityManager.MemoryInfo getAvailableMemory() { + ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE); + ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo(); + activityManager.getMemoryInfo(memoryInfo); + return memoryInfo; +} +</pre> + +<!-- Section 2 #################################################### --> + +<h2 id="code">Use More Memory-Efficient Code Constructs</h2> + +<p> + Some Android features, Java classes, and code constructs tend to + use more memory than others. You can minimize how + much memory your app uses by choosing more efficient alternatives in + your code. +</p> + +<!-- Section 2.1 #################################################### --> + +<h3 id="Services">Use services sparingly</h3> + +<p> + Leaving a service running when it’s not needed is + <strong>one of the worst memory-management + mistakes</strong> an Android app can make. If your app needs a + <a href="{@docRoot}guide/components/services.html">service</a> + to perform work in the background, do not keep it running unless + it needs to run a job. Remember to stop your service when it has completed + its task. Otherwise, you can inadvertently cause a memory leak. +</p> + +<p> + When you start a service, the system prefers to always keep the process + for that service running. This behavior + makes services processes very expensive + because the RAM used by a service remains unavailable to other processes. + This reduces the number of cached processes that the system can keep in + the LRU cache, making app switching less efficient. It can even lead to + thrashing in the system when memory is tight and the system can’t + maintain enough processes to host all the services currently running. +</p> + +<p> + You should generally avoid use of persistent services because of + the on-going demands they place on available memory. Instead, we + recommend that you use an alternative implementation + such as {@link android.app.job.JobScheduler}. For more information about + how to use {@link android.app.job.JobScheduler} to schedule background + processes, see + <a href="/topic/performance/background-optimization.html">Background Optimizations</a>. +<p> + If you must use a service, the + best way to limit the lifespan of your service is to use an {@link + android.app.IntentService}, which finishes + itself as soon as it's done handling the intent that started it. + For more information, read + <a href="{@docRoot}training/run-background-service/index.html">Running in a Background Service</a>. +</p> + +<!-- Section 2.2 #################################################### --> + +<h3 id="DataContainers">Use optimized data containers</h3> + +<p> + Some of the classes provided by the programming language are not optimized for + use on mobile devices. For example, the generic + {@link java.util.HashMap} implementation can be quite memory + inefficient because it needs a separate entry object for every mapping. +</p> + +<p> + The Android framework includes several optimized data containers, including + {@link android.util.SparseArray}, {@link android.util.SparseBooleanArray}, + and {@link android.support.v4.util.LongSparseArray}. + For example, the {@link android.util.SparseArray} classes are more + efficient because they avoid the system's need to + <acronym title="Automatic conversion from primitive types to object classes (such as int to Integer)">autobox</acronym> + the key and sometimes value (which creates yet another object or + two per entry). +</p> + +<p> + If necessary, you can always switch to raw arrays for a really lean data + structure. +</p> + +<!-- Section 2.3 #################################################### --> + +<h3 id="Abstractions">Be careful with code abstractions</h3> + +<p> + Developers often use abstractions simply as a good programming practice, + because abstractions can improve code flexibility and maintenance. + However, abstractions come at a significant cost: + generally they require a fair amount more code that + needs to be executed, requiring more time and + more RAM for that code to be mapped into memory. + So if your abstractions aren't supplying a + significant benefit, you should avoid them. +</p> + +<p> + For example, enums often require more than twice as much memory as static + constants. You should strictly avoid using enums on Android. +</p> + +<!-- Section 2.4 #################################################### --> + +<h3 id="NanoProto">Use nano protobufs for serialized data</h3> + +<p> + <a href="https://developers.google.com/protocol-buffers/docs/overview">Protocol buffers</a> + are a language-neutral, platform-neutral, extensible mechanism + designed by Google for serializing structured data—similar to XML, but + smaller, faster, and simpler. If you decide to use + protobufs for your data, you should always use nano protobufs in your + client-side code. Regular protobufs generate extremely verbose code, which + can cause many kinds of problems in your app such as + increased RAM use, significant APK size increase, and slower execution. +</p> + +<p> + For more information, see the "Nano version" section in the + <a href="https://android.googlesource.com/platform/external/protobuf/+/master/java/README.txt" +class="external-link">protobuf readme</a>. +</p> + +<!-- Section 2.5 #################################################### --> + +<h3 id="churn">Avoid memory churn</h3> + +<p> + As mentioned previously, garbage collections events don't normally affect + your app's performance. However, many garbage collection events that occur + over a short period of time can quickly eat up your frame time. The more time + that the system spends on garbage collection, the less time it has to do + other stuff like rendering or streaming audio. +</p> + +<p> + Often, <em>memory churn</em> can cause a large number of + garbage collection events to occur. In practice, memory churn describes the + number of allocated temporary objects that occur in a given amount of time. +</p> + +<p> + For example, you might allocate multiple temporary objects within a + <code>for</code> loop. Or you might create new + {@link android.graphics.Paint} or {@link android.graphics.Bitmap} + objects inside the + {@link android.view.View#onDraw(android.graphics.Canvas) onDraw()} + function of a view. + In both cases, the app creates a lot of objects quickly at high volume. + These can quickly consume all the available memory in the young generation, + forcing a garbage collection event to occur. +</p> + +<p> + Of course, you need to find the places in your code where + the memory churn is high before you can fix them. Use the tools discussed in + <a href="#AnalyzeRam">Analyze your RAM usage</a> +</p> + +<p> + Once you identify the problem areas in your code, try to reduce the number of + allocations within performance critical areas. Consider moving things out of + inner loops or perhaps moving them into a + <a href="https://en.wikipedia.org/wiki/Factory_method_pattern" class="external-link">Factory</a> + based allocation structure. +</p> + +<!-- Section 3 #################################################### --> + +<h2 id="remove">Remove Memory-Intensive Resources and Libraries</h2> + +<p> + Some resources and libraries within your code can gobble up memory without + you knowing it. Overall size of your APK, including third-party libraries + or embedded resources, can affect how much memory your app consumes. You can + improve your app's memory consumption by removing any redundant, unnecessary, + or bloated components, resources, or libraries from your code. +</p> + +<!-- Section 3.1 #################################################### --> + +<h3 id="reduce">Reduce overall APK size</h3> + +<p> + You can significantly reduce your app's memory usage by reducing the overall + size of your app. Bitmap size, resources, animation frames, and third-party + libraries can all contribute to the size of your APK. + Android Studio and the Android SDK provide multiple tools + to help you reduce the size of your resources and external dependencies. +</p> + +<p> + For more information about how to reduce your overall APK size, see + <a href="{@docRoot}topic/performance/reduce-apk-size.html">Reduce APK Size</a>. +</p> + +<!-- Section 3.2 #################################################### --> + +<h3 id="DependencyInjection">Use caution with dependency injection frameworks</h3> + +<p> + Dependency injection framework such as + <a href="https://code.google.com/p/google-guice/" class="external-link">Guice</a> + or + <a href="https://github.com/roboguice/roboguice" class="external-link">RoboGuice</a> + can simplify the code you write and provide an adaptive environment + that's useful for testing and other configuration changes. However, dependency + frameworks aren't always optimized for mobile devices. +</p> + +<p> + For example, these frameworks tend to initialize processes by + scanning your code for annotations. This which can require significant + amounts of your code to be mapped into RAM unnecessarily. The system + allocates these mapped pages into clean memory so Android can drop them; yet + that can't happen until the pages have remained in memory for a long period + of time. + </p> + +<p> + If you need to use a dependency injection framework in your app, consider + using + <a class="external-link" href="http://google.github.io/dagger/">Dagger</a> + instead. For example, Dagger does not use reflection to scan your app's code. + Dagger's strict implementation means that it can be used in Android apps + without needlessly increasing memory usage. +</p> + +<!-- Section 3.3 #################################################### --> + +<h3 id="ExternalLibs">Be careful about using external libraries</h3> + +<p> + External library code is often not written for mobile environments and + can be inefficient when used + for work on a mobile client. When you decide to use an + external library, you may need to optimize that library for mobile devices. + Plan for that work up-front and analyze the library in terms of code size and + RAM footprint before deciding to use it at all. +</p> + +<p> + Even some mobile-optimized libraries can cause problems due to differing + implementations. For example, one library may use nano protobufs + while another uses micro protobufs, resulting in two different protobuf + implementations in your app. This can happen with different + implementations of logging, analytics, image loading frameworks, + caching, and many other things you don't expect. +</p> + +<p> + Although <a href="{@docRoot}tools/help/proguard.html">ProGuard</a> can + help to remove APIs and resources with the right flags, it can't remove a + library's large internal dependencies. The features that you want in these + libraries may require lower-level dependencies. This becomes especially + problematic when you use an {@link android.app.Activity } subclass from a + library (which will tend to have wide swaths of dependencies), + when libraries use reflection (which is common and means you need to spend a + lot of time manually tweaking ProGuard to get it to work), and so on. +</p> + +<p> + Also avoid using a shared library for just one or two features out of dozens. + You don't want to pull in a large amount of code and overhead that + you don't even use. When you consider whether to use a library, look for + an implementation that strongly matches what you need. Otherwise, you might + decide to create your own implementation. +</p> + diff --git a/docs/html/topic/performance/network-xfer.jd b/docs/html/topic/performance/network-xfer.jd new file mode 100644 index 000000000000..7fe5594ca899 --- /dev/null +++ b/docs/html/topic/performance/network-xfer.jd @@ -0,0 +1,374 @@ +page.title=Reducing Image Download Sizes +page.metaDescription=Improve network performance by optimizing image size. + +meta.tags="performance" +page.tags="performance" + +@jd:body + +<div id="qv-wrapper"> +<div id="qv"> + +<h2>In this document</h2> + <ol> + + <li> + <a href="#uif">Understanding Image Formats</a> + <ul> + <li><a href="#png">PNG</a></li> + <li><a href="#jpg">JPG</a></li> + <li><a href="#webp">WebP</a></li> + </ul> + </li> + <li> + <a href="#sf">Selecting a Format</a></li> + <li><a href="#doqv">Determining Optimal Quality Values</a> + <ul> + <li><a href="#sv">Scalar Values (JPG, WebP only)</a></li> + <li><a href="#butter">Butteraugli</a></li> + </ul> + </li> + <li><a href="#sizes">Serving Sizes</a></li> + </ol> + </div> +</div> + +<p> +Most download traffic consists of images. As a result, the smaller you can make +your downloadable images, the better a network experience your app can provide +for users. This page provides guidance on making image files smaller and more +network-friendly. +</p> + +<h2 id="uif">Understanding Image Formats</h2> + +<p>Android apps typically use images that are in one or more of the following file +formats: PNG, JPG, and WebP. For each of these formats, there are steps you can +take to reduce image sizes. +</p> + +<h3 id="png">PNG</h3> + +<p> +A key to making your PNG files smaller is reducing the number of unique +colors used in each row of pixels that comprises the image. By using fewer +colors, you improve the compression potential at all of the other stages of +the pipeline. +</p> + +<p> +Reducing the number of unique colors makes a significant difference because PNG +compression effectiveness is partly a function of the degree to which +horizontally adjacent pixel colors vary. Thus, reducing the number of unique +colors in each row of your PNG images can help in reducing their file sizes. +</p> + +<p> +When deciding whether to pursue this strategy, you should keep in mind that +reducing the number of unique colors effectively amounts to applying a lossy +encoding stage to the image. However, an encoding tool may not be a good +judge of how bad a seemingly small error looks to the human eye. Therefore, +you should perform this work manually in order to help ensure +the right balance between efficient compression and acceptable image quality. +</p> + +<p> +There are two particularly useful approaches you can take: striving for indexed +formats, and applying vector quantization. +</p> + + +<h4 id="strive">Strive for indexed formats</h4> + +<p> +Any attempt at color reduction should start with trying to optimize your colors +so that you can use the INDEXED format when exporting the image as a PNG. The +INDEXED color mode works by choosing the best 256 colors to use, and replacing +all pixel values with indices into that color palette. The result is a +reduction from 16 million (potential) colors to only 256 colors: from 3 (without +transparency) or 4 (with transparency) bytes per pixel to 1 byte per pixel. +This change is a significant first-step file size reduction. +</p> + +<p> +Figure 1 shows shows an image and its indexed variant. +</p> + + <img src="{@docRoot}topic/performance/images/beforeafterindexed.png"> + <p class="img-caption"> +Figure 1. An image before and after conversion to the INDEXED format. + </p> + + +<p> +Figure 2 shows the color palette for the image in Figure 1: +</p> + + <img src="{@docRoot}topic/performance/images/palette.png"> + <p class="img-caption"> +Figure 2. The color palette for the image in Figure 1. + </p> + +<p> +Representing your image as a paletted image goes a long way toward +significantly improving the file size, so it's worth investigating if the +majority of your images can be converted. +</p> + +<p> +Of course, not every image can be accurately represented with only 256 colors. +Some images, for example, might need 257, 310, 512, or 912 colors to +look correct. In such cases, vector quantization can also be helpful. +</p> + +<h4 id="vq">Vector quantization</h4> + +<p> +The process of creating an indexed image may be better described as vector +quantization (VQ). VQ serves as a rounding process for multidimensional +numbers. In this process, all the colors in your image get grouped based upon +their similarity. For a given group, all colors in that group are replaced by a +single <em>center point</em> value, which minimizes error for colors in that +cell (or "site" if you're using the Voronoi terminology). In Figure 3, +the green dots represent input colors, and the red dots are the center points +that replace the input colors. Each cell is bounded by blue lines. +</p> + + <img src="{@docRoot}topic/performance/images/vq.gif"> + <p class="img-caption"> +Figure 3. Applying vector quantization to the colors in an image. +</p> + +<p> +The result of applying VQ to an image reduces the number of unique colors, +replacing each group of colors with a single color that's "pretty close" +in visual quality. +</p> + +<p> +This technique also allows you to define the maximum number of unique colors in +your image. For example, Figure 4 shows the a parrot head in 16.7 million colors +(24 bits per pixel, or bpp) alongside a version that only allows only +16 (3 bpp) unique colors to be used. +</p> + + <img src="{@docRoot}topic/performance/images/parrot.png"> + <p class="img-caption"> +Figure 4. Image before and after application of vector quantification. + </p> + +<p> +Immediately, you can see that there's a loss of quality; most of the gradient +colors have been replaced, imparting a banding effect to the image. This image +needs more than 16 unique colors. +</p> + +<p> +Setting up a VQ step in your pipeline can help you get a better sense of the +true number of unique colors that your image uses, and can help you reduce them +significantly. There are a number of readily available tools that you can use +to help you implement this technique. +</p> + +<h3 id="jpg">JPG</h3> + +<p> +If you are using JPG images, there are several small changes you can make that +potentially provide significant file-size savings. These include: +</p> + +<ul> + <li> +Producing a smaller file size through different encoding methods (without +impacting quality). + </li> + + <li> +Adjusting quality slightly in order to yield better compression. + </li> +</ul> + +<p>Pursuing these strategies can often net you file-size reductions of up to +25%. +</p> + +<p> +When choosing tools, remember that photo exporting tools can +insert unnecessary metadata, such as GPS information, into your images. At +a minimum, try to leverage existing tools to help strip out this information +from your files. +</p> + +<h3 id="webp">WebP</h3> + +<p> +WebP is a newer image format supported from Android 4.2.1 (API level 17). This +format provides superior lossless and lossy compression for images on the web. +Using WebP, developers can create smaller, richer images. WebP lossless image +files are, on average, +<a href="https://developers.google.com/speed/webp/docs/webp_lossless_alpha_study#conclusions"> +26% smaller</a> than PNGs. These image files also support +transparency (also known as alpha channel) at a cost of just +<a href="https://developers.google.com/speed/webp/docs/webp_lossless_alpha_study#results"> +22% more</a> bytes. +</p> + +<p> +WebP lossy images are +<a href="https://developers.google.com/speed/webp/docs/webp_study#experiment_1_webp_vs_jpeg_at_equal_ssim_index"> +25-34% smaller</a> than comparable JPG images at equivalent +<a href="https://en.wikipedia.org/wiki/Structural_similarity">SSIM</a> +quality indices. For cases when lossy RGB compression is acceptable, lossy +WebP also supports transparency, typically producing file sizes 3 times smaller +than PNG. +</p> + +<p> +For more information about WebP, visit the +<a href="https://developers.google.com/speed/webp/">WebP site</a>. +</p> + +<h2 id="sf">Selecting a Format</h2> + +<p> +Different image formats are suitable for different types of images. JPG and PNG +have very different compression processes, and they produce quite different +results. +</p> + +<p> +The decision between PNG and JPG often comes down to the complexity of the +image itself. Figure 5 shows two images that come out quite differently +depending on which compression scheme the developer applies. The image on the +left has many small details, and thus compresses more efficiently with JPG. The +image on the right, with runs of the same color, compresses more efficiently +with PNG. +</p> + + <img src="{@docRoot}topic/performance/images/comparison.png"> + <p class="img-caption"> +Figure 5. Suitable cases for JPG vs. PNG + </p> + + +<p> +WebP as a format can support both lossy and lossless modes, making it an ideal +replacement for both PNG and JPG. The only thing to keep in mind is that it +only has native support on devices running Android 4.2.1 (API level 17) and +higher. Fortunately, the large +<a +href="https://developer.android.com/about/dashboards/index.html#Platform"> +majority of devices</a> satisfy that requirement. +</p> + +<p> +Figure 6 provides a simple visualization to help you decide which compression +scheme to use. +</p> + + <img src="{@docRoot}topic/performance/images/decisions.png"> + <p class="img-caption"> +Figure 6. Deciding on a compression scheme + </p> + +<h2 id="doqv">Determining Optimal Quality Values</h2> + +<p> +There are several techniques you can use to achieve the right balance between +compression and image quality. One technique uses scalar values and therefore +only works for JPG and WebP. The other technique takes advantage of the +Butteraugli library, and is usable for all image formats. +</p> + +<h3 id="sv">Scalar values (JPG and WebP only)</h3> + +<p> +The power of JPG and WebP comes from the fact that you can use a scalar value +to balance quality against file size. The trick is finding out what the correct +quality value is for your image. Too low a quality level produces a small file +at the cost of image quality. Too high a quality level increases file size +without providing a noticeable benefit to the user. +</p> + +<p> +The most straightforward solution is to pick some non-maximum value, and use +that value. However, be aware that the quality value affects every image +differently. While a quality of 75%, for example, may look fine on most images, +there may be some cases do not fare as well. You should make sure to test your +chosen maximum value against a representative sample of images. Also, make +sure to perform all of your tests against the original images, and not on +compressed versions. +</p> + +<p> +For large media applications that upload and re-send millions of JPGs a day, +hand-tuning for each asset is impractical. You might address this challenge by +specifying several different quality levels, according to image category. For +example, you might set 35% as the quality setting for thumbnails, since a +smaller image hides more compression artifacts. +</p> + +<h3 id="butter">Butteraugli</h4> + +<p> +The Butteraugli project is a library to test an image's Psychovisual Error +Threshold: the point at which a viewer starts to notice image degradation. In +other words, this project attempts to quantify how distorted your compressed +image is. +</p> + +<p> +Butteraugli allows you to define a goal for visual quality, and then run PNG, +JPG, WebP lossy, and WebP lossless compressions. You can then choose the image +that is the best balance of file size and Butteraugli level. Figure 7 shows an +example of how Butteraugli was used to find the minimal JPG quality level +before the visual distortion was high enough for a user could perceive a +problem; the result is a roughly 65% reduction in file size. +</p> + + <img src="{@docRoot}topic/performance/images/moarparrots.png"> + <p class="img-caption"> +Figure 7. An image before and after application of Butteraugli technology. + </p> + +<p> +Butteraugli allows you to proceed based on either output or input. That is, you +can look for the lowest quality setting before a user perceives noticeable +distortion in the resulting image, or you can iteratively set image-distortion +levels to learn their associated quality levels. +</p> + +<h2 id="sizes">Serving Sizes</h2> + +<p> +It is tempting to keep only a single resolution of an image on a server. When a +device accesses the image, the server serves it at that one resolution and +leaves downscaling to the device. +</p> + +<p> +This solution is convenient for the developer, but potentially painful for the +user, because the solution forces the user to download much more data than they +need. + +You should instead store multiple sizes of images, and serve the size that is +most appropriate for a particular use case. For example, for a thumbnail, +serving an actual thumbnail image instead of serving and downscaling a +full-size version consumes much less network bandwidth +</p> + +</p> +This approach is good for download speed, and is less costly for users who may +be using limited or metered data plans. Proceeding like this also results in +the image's taking less space on the device and in main memory. In the +case of large images, such as 4K ones, this approach also saves the device +from having to resize images before loading them. +</p> + +<p> +Implementing this approach requires that you have a backend image service to +provide images at various resolutions with proper caching. There are existing +services that can provide help with this task. For example, +<a href="https://cloud.google.com/appengine/">App Engine</a> comes +with image resizing functionality already installed. +</p> diff --git a/docs/html/topic/performance/power/battery-historian.jd b/docs/html/topic/performance/power/battery-historian.jd new file mode 100644 index 000000000000..79ea59d21419 --- /dev/null +++ b/docs/html/topic/performance/power/battery-historian.jd @@ -0,0 +1,247 @@ +page.title=Analyzing Power Use with Battery Historian +page.metaDescription=Improve network performance by optimizing image size. + +meta.tags="power" +page.tags="power" + +@jd:body + +<div id="qv-wrapper"> +<div id="qv"> + +<h2>In this document</h2> + <ol> + <li> + <a href="#sv">System-wide View</a> + </li> + <li> + <a href="#asd">App-Specific Data</a> + </li> + <li> + <a href="#usecases">Other Cases Where Battery Historian Can Help</a> + </li> + </ol> +<h2>See also</h2> + <ol> + <li> + <a href="https://github.com/google/battery-historian">Battery Historian + on GitHub</a> + </li> + + <li> + <a href="https://developer.android.com/studio/profile/battery-historian.html"> + Batterystats and Battery Historian Walkthrough + </li> + + <li> + <a href="https://youtu.be/VC2Hlb22mZM?list=PLOU2XLYxmsILe6_eGvDN3GyiodoV3qNSC&t=2063" + target="_blank"> + Battery Historian talk at Google I/O 2016</a> + </li> + </ol> + </div> +</div> + +<p> +The Battery Historian tool provides insight into a device’s battery consumption +over time. At a system-wide level, the tool visualizes power-related events from +the system logs in an HTML representation. At an app-specific level, the tool +provides a variety of data that can help you identify battery-draining app +behavior. +</p> + +<p> +This document describes some of the ways you can use Battery Historian +to learn about battery-consumption patterns. The document begins by explaining +how to read the system-wide data that Battery Historian reports. Then, +it presents ways in which you can use Battery Historian to diagnose +and troubleshoot your own app's behavior related to battery consumption. +Last, it offers several tips on scenarios in which Battery Historian may be +particularly useful. +</p> + +<h2 id="sv">System-wide View</h2> + +<p> +The Battery Historian tool provides a system-wide visualization of various +app and system behaviors, along with their correlation against battery +consumption over time. This view, shown in Figure 1, can help you +diagnose and identify power use issues with your app. +</p> + + <img src="{@docRoot}topic/performance/images/generic-timeline.png"> + <p class="img-caption"> +<strong>Figure 1.</strong> +Battery Historian’s display of system-wide events affecting power +consumption. + </p> + +<p> +Of particular interest in this figure is the black, horizontal, downward trend +line representing Battery Level, measured on the y-axis. For example, at the +very beginning of the Battery Level line, at approximately 6:50 AM, the +visualization shows a relatively steep drop in battery level. +</p> + +<p> +Figure 2 provides a close-up of that part of the display. +</p> + + <img src="{@docRoot}topic/performance/images/s-generic-closeup.png"> + <p class="img-caption"> +<strong>Figure 2.</strong> +A close-up of the Battery Historian timeline from roughly 6:50 AM to 7:20 AM. + </p> + +<p> +At the very beginning of the Battery Level line, as battery decline steeply, +the display shows three things happening: The CPU is running, an app has +acquired a wakelock, and the screen is on. In this way, Battery Historian helps +you understand what events are happening when battery consumption is high. You +can then target these behaviors in your app and investigate whether there are +related optimizations you can make. +</p> + +<p> +The system-wide visualization can provide other clues, as well. For instance, if +it shows that the mobile radio is frequently being turned off and on, there may +be an opportunity to optimize this behavior through <a href=”intelligent +scheduling page”>intelligent scheduling APIs</a> such as JobScheduler or +Firebase Job Dispatcher. +</p> + +<p> +The next section explains how to investigate behavior and events specific to +your own app. +</p> + +<p> +<h2 id="asd">App-Specific Data</h2> +</p> + +<p> +In addition to the macro-level data provided by the system-wide view, Battery +Historian also provides tables and some visualization of data specific to each +app running on your device. The tabular data includes: +</p> + +<ul> + <li>The app’s estimated power use on the device.</li> + <li>Network information.</li> + <li>Wakelocks.</li> + <li>Services.</li> + <li>Process info.</li> +</ul> + +<p> +The tables provide two dimensions of data about your app. First, you can look +up where your app’s power usage ranks compared to other apps. To do so, click +<em>Device Power Estimates</em> table under <em>Tables</em>. This example +examines a fictional app called Pug Power. +</p> + + <img src="{@docRoot}topic/performance/images/app-rankings.png"> + <p class="img-caption"> +<strong>Figure 3.</strong> Investigating which apps consume the most power. + </p> + +<p> +The table in Figure 3 reveals that Pug Power is the ninth biggest consumer of +battery power on this device, and the third biggest app that is not part of the +OS. This data suggests that this app bears deeper investigation. +</p> + +<p> +To look up the data for a specific app, enter its package name into the lower +of the two dropdown menus under <em>App Selection</em>, located under the left +side of the visualization. +</p> + + <img src="{@docRoot}topic/performance/images/dropdown.png"> + <p class="img-caption"> +<strong>Figure 4.</strong> Entering a specific app whose data to view. + </p> + +<p> +When you select a specific app, the following data visualization categories +change to display app-specific data instead of system-wide data: +</p> + +<ul> + <li>SyncManager.</li> + <li>Foreground process.</li> + <li>Userspace Wakelock.</li> + <li>Top app.</li> + <li>JobScheduler.</li> + <li>Activity Manager Proc.</li> +</ul> + +The SyncManager and JobScheduler visualizations immediately make it obvious if +your app performs syncs and executes jobs more frequently than necessary. In +doing so, they can quickly reveal an opportunity to optimize your app’s +behavior for improved battery performance. + +<p> +You can also obtain one more piece of app-specific visualization data, +<em>Userspace Wakelock</em>. To include this information in the bug report, +enter the following command in your terminal window: +</p> + +<pre> +$ adb shell dumpsys batterystats --enable full-wake-history +</pre> + +<p class="note"> +<strong>Note:</strong> From Android 6.0 (API level 23), the platform includes +Doze functionality, which imposes certain optimizations on apps. For example, +Doze batches jobs to take place during brief maintenance windows, regardless of +how JobScheduler has scheduled them. +</p> + +<p> +Figures 5 and 6 show data for Pug Power: Figure 5 +shows the visualization of +the app-specific data, and Figure 6 shows the corresponding tabular data. +</p> + + <img src="{@docRoot}topic/performance/images/pug-visualization.png"> + <p class="img-caption"> +<strong>Figure 5.</strong> Visualization of data for fictional app Pug Power. + </p> + + <img src="{@docRoot}topic/performance/images/pugspecificdata.png"> + <p class="img-caption"> +<strong>Figure 6.</strong> Tabular data for the fictional Pug Power app. + </p> + +<p> +A look at the visualization does not show anything immediately obvious. +The JobScheduler line shows that the app has no jobs scheduled. The SyncManager +line shows that the app has not performed any syncs. +</p> + +<p> +However, examination of the <em>Wakelocks</em> segment of the tabular data +reveals that Pug Power acquires wakelocks totaling over an hour. This unusual +and costly behavior can account for the app’s high level of power consumption. +This piece of information helps the developer target an area where optimization +is likely to greatly help. In this case, why does the app acquire so much +wakelock time, and how can the developer ameliorate this behavior? +</p> + +<h2 id="usecases">Other Cases Where Battery Historian Can Help</h2> + +<p> +There are many other cases in which Battery Historian can help you diagnose +opportunities for improving battery behavior. For example, Battery Historian +can tell you if your app is: +</p> + +<ul> + <li>Firing wakeup alarms overly frequently (every 10 seconds or less).</li> + <li>Continuously holding a GPS lock.</li> + <li>Scheduling jobs every 30 seconds or less.</li> + <li>Scheduling syncs every 30 seconds or less.</li> + <li>Using the cellular radio more frequently than you expect.</li> +</ul> + diff --git a/docs/html/topic/performance/power/index.jd b/docs/html/topic/performance/power/index.jd new file mode 100644 index 000000000000..88addcea1f71 --- /dev/null +++ b/docs/html/topic/performance/power/index.jd @@ -0,0 +1,125 @@ +page.title=Optimizing for Battery Life +page.metaDescription=Learn how to help your app go easier on the battery. + +meta.tags="performance" +page.tags="performance" + +@jd:body + +<div id="qv-wrapper"> + <div id="qv"> + <h2> + In this document + </h2> + <ol> + <li> + <a href="#lazy">Lazy First</a> + </li> + <li> + <a href="#features">Platform Features</a> + </li> + <li> + <a href="#toolery">Tooling</a> + </li> + </ol> + </div> +</div> + +<p>Battery life is the single most important aspect of the mobile user +experience. A device without power offers no functionality at all. +For this reason, it is critically important that apps be as respectful of +battery life as possible.</p> + +<p>There are three important things to keep in mind in keeping your app +power-thrifty:</p> +<ul> +<li>Make your apps <em>Lazy First</em>.</li> +<li>Take advantage of platform features that can help manage your app's battery +consumption.</li> +<li>Use tools that can help you identify battery-draining culprits.</li> +</ul> + +<h2 id="lazy">Lazy First</h2> + +<p>Making your app Lazy First means looking for ways to reduce and optimize +operations that are particularly battery-intensive. The core questions +underpinning Lazy First design are: + +<ul> + + <li><strong>Reduce:</strong> Are there redundant operations your app can cut +out? For example, can it cache downloaded data instead of repeatedly waking + up the radio to re-download the data?</li> + + <li><strong>Defer:</strong> Does an app need to perform an action right + away? For example, + can it wait until the device is charging before it backs data up to the + cloud?</li> + + <li><strong>Coalesce:</strong> Can work be batched, instead of putting the + device + into an active state many times? For example, is it really necessary for + several dozen apps to each turn on the radio at separate times to send + their messages? Can the messages instead be transmitted during a + single awakening of the radio?</li> +</ul> + +<p> +You should ask these questions when it comes to using the CPU, +the radio, and the screen. Lazy First design is often a good way +to tame these battery killers. +</p> + +<p> +To help you achieve these and other efficiencies, the Android platform +provides a number of features to help maximize battery life. +</p> + +<h2 id="features">Platform Features</h2> + +<p> +Broadly speaking, the Android platform provides two categories of help +for you to optimize your app's battery use. First, it provides several +APIs that you can implement in your app. You can learn more about these APIs in +<a href="/topic/performance/scheduling.html">Intelligent Job Scheduling</a> +and <a href="/performance/power/network/index.html"> +Network Use and Battery Consumption</a>. +</p> + +<p> +There are also internal mechanisms in the platform to help conserve +battery life. While they are not APIs that you implement programmatically, +you should still be aware of them so that your app can leverage them +successfully. For more information, see +<a href="/training/monitoring-device-state/doze-standby.html">Doze and +App Standby</a>.</p> + +<p> +You can get even more benefit out of these features by using the tools +available for the platform to discover the parts of your app that consume +the most power. Finding what to target is a big step toward +successful optimization. +</p> + +<h2 id ="toolery">Tooling</h2> + +<p>There are tools for Android, including +<a href="/studio/profile/dev-options-rendering.html">Profile GPU Rendering</a> +and <a class="external-link" +href="https://github.com/google/battery-historian">Battery Historian</a> +to help you identify areas that you can optimize for better battery life. +Take advantage of these tools to target areas where you can apply the +principles of Lazy First. +</p> + +<section class="dac-section dac-small" id="latest-games"><div class="wrap"> + <h2 class="norule" style="margin:0 0">More resources</h2> + <div class="resource-widget resource-flow-layout col-16" + data-query="collection:develop/performance/landing" + data-sortOrder="random" + data-cardSizes="6x6" + data-maxResults="24" + data-items-per-page="24" + data-initial-results="3"></div> + </div> +</section> diff --git a/docs/html/topic/performance/rendering/index.jd b/docs/html/topic/performance/rendering/index.jd new file mode 100644 index 000000000000..1b16df035376 --- /dev/null +++ b/docs/html/topic/performance/rendering/index.jd @@ -0,0 +1,60 @@ +page.title=Rendering +page.article=true + +page.tags=battery +page.metaDescription=Learn how to optimize your app's rendering performance. + +@jd:body + + +<iframe width="448" height="252" + src="//www.youtube.com/embed/wIy8g8yNhNk?autohide=1&showinfo=0" + frameborder="0" allowfullscreen="" + style="float: right; margin: 0 0 20px 20px;"></iframe> + +<p> + A key aspect of your app that influences your users' perception of quality is + the smoothness with which it renders images and text to the screen. It is + important to avoid jank and sluggish responsiveness when your app is drawing + to the screen. +</p> + +<p> + This section helps you learn several ways to optimize your app's rendering + performance: reducing overdraw, optimizing view hierarchies, and taking + advantage of the Profile GPU tool. +</p> + +<h2>Rendering Actions</h2> + +<dl> + <dt> + <strong><a href="overdraw.html"> + Reducing Overdraw</a></strong> + </dt> + <dd> + Minimize the number of times you app redraws the same pixel in a single + frame. + </dd> + + <dt> + <strong><a href="optimizing-view-hierarchies.html"> + Performance and View Hierarchies</a></strong> + </dt> + <dd> + Make sure your layout and measurement are executing efficiently, and + avoid double taxation. + </dd> + + + <dt> + <strong><a href="profile-gpu.html"> + Analyzing with Profile GPU Rendering</a></strong> + </dt> + <dd> + Take advantage of this on-device tool to identify bottlenecks that + may be slowing your app's rendering down. + </dd> + + +</dl> diff --git a/docs/html/topic/performance/optimizing-view-hierarchies.jd b/docs/html/topic/performance/rendering/optimizing-view-hierarchies.jd index 27d3d163f853..27d3d163f853 100644 --- a/docs/html/topic/performance/optimizing-view-hierarchies.jd +++ b/docs/html/topic/performance/rendering/optimizing-view-hierarchies.jd diff --git a/docs/html/topic/performance/rendering/overdraw.jd b/docs/html/topic/performance/rendering/overdraw.jd new file mode 100644 index 000000000000..c1feff57a26b --- /dev/null +++ b/docs/html/topic/performance/rendering/overdraw.jd @@ -0,0 +1,197 @@ +page.title=Reducing Overdraw +page.metaDescription=Improve performance by reducing unnecessary rendering. + +meta.tags="performance" +page.tags="performance" + +@jd:body + +<div id="qv-wrapper"> +<div id="qv"> + +<h2>In this document</h2> + <ol> + + <li> + <a href="#understanding">Understanding Overdraw</a> + </li> + <li> + <a href="#finding">Finding Overdraw Problems</a> + </li> + <li> + <a href="#fixing">Fixing Overdraw</a> + </li> + </ol> + </div> +</div> + +<p> +An app may draw the same pixel more than once within a single frame, an event +called <em>overdraw</em>. Overdraw is usually unnecessary, and best +eliminated. It manifests itself as a performance problem by wasting GPU time to +render pixels that don't contribute to what the user sees on the screen. +</p> + +<p> +This document explains overdraw: what it is, how to diagnose it, and actions you +can take to eliminate or mitigate it. +</p> + +<h2 name="understanding">Understanding Overdraw</h2> + +<p> +Overdraw refers to the system's drawing a pixel on the screen multiple times +in a single frame of rendering. For example, if we have a bunch of stacked UI +cards, each card hides a portion of the one below it. +</p> + +<p> +However, the system still needs to draw even the hidden portions of the cards +in the stack. This is because stacked cards are rendered according to the +<a class="external-link" +href="https://en.wikipedia.org/wiki/Painter%27s_algorithm">painter's +algorithm</a>: that is, in back-to-front order. +This sequence of rendering allows the system to apply proper alpha blending to +translucent objects such as shadows. +</p> + +<h2 name="finding">Finding Overdraw Problems</h2> + +<p> +The platform offers several tools to help you determine if overdraw is +affecting your app's performance. These tools are available right on the device, +and accessible by turning on <strong>Developer Settings</strong></a> under +<em>Settings</em>. For more information about device developer settings, see +<a href="/studio/run/device.html#developer-device-options">Run Apps on a +Hardware Device</a>. +</p> + +<h3 id="dgot">Debug GPU overdraw tool</h3> + +<p> +The Debug GPU Overdraw tool uses color-coding to show the number of times your +app draws each pixel on the screen. The higher this count, the +more likely it is that overdraw affects your app's performance. +</p> + +<p> +For more information on how to use the tool, refer to the related +<a href="/studio/profile/dev-options-overdraw.html">walkthrough</a> +and +<a href="https://io2015codelabs.appspot.com/codelabs/android-performance-debug-gpu-overdraw#1"> +codelab</a>. +</p> + +<h3 id="pgrt">Profile GPU rendering tool</h3> + +<p> +The Profile GPU Rendering tool displays, as a scrolling histogram, the time +each stage of the rendering pipeline takes to display a single frame. The +<em>Process</em> part of each bar, indicated in orange, shows when the system +is swapping buffers; this metric provides important clues about overdraw. +</p> + +<p> +On less performant GPUs, available fill-rate (the speed at which the GPU can +fill the frame buffer) can be quite low. As the number of +pixels required to draw a frame increases, the GPU may take longer to process +new commands, and ask the rest of the system to wait until it can catch up. +The <em>Process</em> bar shows that this spike happens as the GPU gets +overwhelmed trying to draw pixels as fast as possible. Issues other than +raw numbers of pixels may also cause this metric to spike. For example, +if the Debug GPU Overdraw tool shows heavy overdraw and <em>Process</em> spikes, +there's likely an issue with overdraw. +</p> + +<p class="note"><strong>Note: </strong>The +<a href="https://developer.android.com/studio/profile/dev-options-rendering.html"> +Profile GPU Rendering</a> tool does not +work with apps that use the NDK. This is because the system pushes framework +messages to the background whenever OpenGL takes a full-screen context. In +such cases, you may find a profiling tool provided by the GPU manufacturer +helpful.</p> + +<h2 name="fixing">Fixing Overdraw</h2> + +<p> +There are several strategies you can pursue to reduce or eliminate overdraw: +</p> + +<ul> + <li>Removing unneeded backgrounds in layouts.</li> + <li>Flattening the view hierarchy.</li> + <li>Reducing transparency.</li> +</ul> + +<p> +This section provides information about each of these approaches. +</p> + +<h3 id="rubil">Removing unneeded backgrounds in layouts</h3> + +<p> +By default, a layout does not have a background, which means it does not render +anything directly by itself. When layouts do have backgrounds, however, they may +contribute to overdraw. +</p> + +<p> +Removing unnecessary backgrounds is a quick way of improving rendering +performance. An unnecessary background may never be visible because it's +completely covered by everything else the app is drawing on top of that +view. For example, the system may entirely cover up a parent's +background when it draws child views on top of it. +</p> + +<p> +To find out why you're overdrawing, walk through the hierarchy in +the <a href="/studio/profile/hierarchy-viewer.html">Hierarchy Viewer</a> tool. +As you do so, look out for any backgrounds you can eliminate because +they are not visible to the user. Cases where many containers share a +common background color offer another opportunity to eliminate unneeded +backgrounds: You can set the window background to the main background color +of your app, and leave all of the containers above it with no background values +defined. +</p> + +<h3 id="fvh">Flattening view hierarchy</h3> + +<p> +Modern layouts make it easy to stack and layer views to produce beautiful +design. However, doing so can degrade performance by resulting in overdraw, +especially in scenarios where each stacked view object is opaque, requiring the +drawing of both seen and unseen pixels to the screen. +</p> + +<p> +If you encounter this sort of issue, you may be able to improve performance by +optimizing your view hierarchy to reduce the number of overlapping UI objects. +For more information about how to accomplish this, see +<a href="/topic/performance/optimizing-view-hierarchies.html">Optimizing View +Hierarchies</a>. +</p> + +<h3 id="rt">Reducing transparency</h3> + +<p> +Rendering of transparent pixels on screen, known as alpha rendering, is a key +contributor to overdraw. Unlike standard overdraw, +in which the system completely hides existing drawn pixels by drawing +opaque pixels on top of them, transparent +objects require existing pixels to be drawn first, so that the right blending +equation can occur. Visual effects like transparent animations, fade-outs, and +drop shadows all involve some sort of transparency, and can therefore contribute +significantly to overdraw. You can improve overdraw in these situations by +reducing the number of transparent objects you render. For example, you can get +gray text by drawing black text in a {@link android.widget.TextView} with a +translucent alpha value set on it. But you can get the same effect with far +better performance by simply drawing the text in gray. +</p> + +<p> +To learn more about performance costs that transparency imposes throughout the +entire drawing pipeline, watch the video +<a href="https://www.youtube.com/watch?v=wIy8g8yNhNk&index=46&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE"> +Hidden Costs of Transparency</a>. +</p> + diff --git a/docs/html/topic/performance/rendering/profile-gpu.jd b/docs/html/topic/performance/rendering/profile-gpu.jd new file mode 100644 index 000000000000..fc9877772610 --- /dev/null +++ b/docs/html/topic/performance/rendering/profile-gpu.jd @@ -0,0 +1,406 @@ +page.title=Analyzing with Profile GPU Rendering +page.metaDescription=Use the Profile GPU tool to help you optimize your app's rendering performance. + +meta.tags="power" +page.tags="power" + +@jd:body + +<div id="qv-wrapper"> +<div id="qv"> + +<h2>In this document</h2> + <ol> + <li> + <a href="#visrep">Visual Representation</a></li> + </li> + + <li> + <a href="#sam">Stages and Their Meanings</a> + + <ul> + <li> + <a href="#sv">Input Handling</a> + </li> + <li> + <a href="#asd">Animation</a> + </li> + <li> + <a href="#asd">Measurement/Layout</a> + </li> + <li> + <a href="#asd">Drawing</a> + </li> + </li> + <li> + <a href="#asd">Sync/Upload</a> + </li> + <li> + <a href="#asd">Issuing Commands</a> + </li> + <li> + <a href="#asd">Processing/Swapping Buffer</a> + </li> + <li> + <a href="#asd">Miscellaneous</a> + </li> + </ul> + </li> + </ol> + </div> +</div> + +<p> +The <a href="/studio/profile/dev-options-rendering.html"> +Profile GPU Rendering</a> tool indicates the relative time that each stage of +the rendering pipeline takes to render the previous frame. This knowledge +can help you identify bottlenecks in the pipeline, so that you +can know what to optimize to improve your app's rendering performance. +</p> + +<p> +This page briefly explains what happens during each pipeline stage, and +discusses issues that can cause bottlenecks there. Before reading +this page, you should be familiar with the information presented in the +<a href="/studio/profile/dev-options-rendering.html">Profile GPU +Rendering Walkthrough</a>. In addition, to understand how all of the +stages fit together, it may be helpful to review +<a href="https://www.youtube.com/watch?v=we6poP0kw6E&index=64&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE"> +how the rendering pipeline works.</a> +</p> + +<h2 id="#visrep">Visual Representation</h2> + +<p> +The Profile GPU Rendering tool displays stages and their relative times in the +form of a graph: a color-coded histogram. Figure 1 shows an example of +such a display. +</p> + + <img src="{@docRoot}topic/performance/images/bars.png"> + <p class="img-caption"> +<strong>Figure 1.</strong> Profile GPU Rendering Graph + </p> + +</p> + +<p> +Each segment of each vertical bar displayed in the Profile GPU Rendering +graph represents a stage of the pipeline and is highlighted using a specific +color in +the bar graph. Figure 2 shows a key to the meaning of each displayed color. +</p> + + <img src="{@docRoot}topic/performance/images/s-profiler-legend.png"> + <p class="img-caption"> +<strong>Figure 2.</strong> Profile GPU Rendering Graph Legend + </p> + +<p> +Once you understand what each color signfiies, +you can target specific aspects of your +app to try to optimize its rendering performance. +</p> + +<h2 id="sam">Stages and Their Meanings</a></h2> + +<p> +This section explains what happens during each stage corresponding +to a color in Figure 2, as well as bottleneck causes to look out for. +</p> + + +<h3 id="ih">Input Handling</h3> + +<p> +The input handling stage of the pipeline measures how long the app +spent handling input events. This metric indicates how long the app +spent executing code called as a result of input event callbacks. +</p> + +<h4>When this segment is large</h4> + +<p> +High values in this area are typically a result of too much work, or +too-complex work, occurring inside the input-handler event callbacks. +Since these callbacks always occur on the main thread, solutions to this +problem focus on optimizing the work directly, or offloading the work to a +different thread. +</p> + +<p> +It’s also worth noting that {@link android.support.v7.widget.RecyclerView} +scrolling can appear in this phase. +{@link android.support.v7.widget.RecyclerView} scrolls immediately when it +consumes the touch event. As a result, +it can inflate or populate new item views. For this reason, it’s important to +make this operation as fast as possible. Profiling tools like Traceview or +Systrace can help you investigate further. +</p> + +<h3 id="at">Animation</h3> + +<p> +The Animations phase shows you just how long it took to evaluate all the +animators that were running in that frame. The most common animators are +{@link android.animation.ObjectAnimator}, +{@link android.view.ViewPropertyAnimator}, and +<a href="/training/transitions/overview.html">Transitions</a>. +</p> + +<h4>When this segment is large</h4> + +<p> +High values in this area are typically a result of work that’s executing due +to some property change of the animation. For example, a fling animation, +which scrolls your {@link android.widget.ListView} or +{@link android.support.v7.widget.RecyclerView}, causes large amounts of view +inflation and population. +</p> + +<h3 id="ml">Measurement/Layout</h3> + +<p> +In order for Android to draw your view items on the screen, it executes +two specific operations across layouts and views in your view hierarchy. +</p> + +<p> +First, the system measures the view items. Every view and layout has +specific data that describes the size of the object on the screen. Some views +can have a specific size; others have a size that adapts to the size +of the parent layout container +</p> + +<p> +Second, the system lays out the view items. Once the system calculates +the sizes of children views, the system can proceed with layout, sizing +and positioning the views on the screen. +</p> + +<p> +The system performs measurement and layout not only for the views to be drawn, +but also for the parent hierarchies of those views, all the way up to the root +view. +</p> + +<h4>When this segment is large</h4> + +<p> +If your app spends a lot of time per frame in this area, it is +usually either because of the sheer volume of views that need to be +laid out, or problems such as +<a href="/topic/performance/optimizing-view-hierarchies.html#double"> +double taxation</a> at the wrong spot in your +hierarchy. In either of these cases, addressing performance involves +<a href="/topic/performance/optimizing-view-hierarchies.html">improving +the performance of your view hierarchies</a>. +</p> + +<p> +Code that you’ve added to +{@link android.view.View#onLayout(boolean, int, int, int, int)} or +{@link android.view.View#onMeasure(int, int)} +can also cause performance +issues. <a href="/studio/profile/traceview.html">Traceview</a> and +<a href="/studio/profile/systrace.html">Systrace</a> can help you examine +the callstacks to identify problems your code may have. +</p> + +<h3 id="draw">Drawing</h3> + +<p> +The draw stage translates a view’s rendering operations, such as drawing +a background or drawing text, into a sequence of native drawing commands. +The system captures these commands into a display list. +</p> + +<p> +The Draw bar records how much time it takes to complete capturing the commands +into the display list, for all the views that needed to be updated on the screen +this frame. The measured time applies to any code that you have added to the UI +objects in your app. Examples of such code may be the +{@link android.view.View#onDraw(android.graphics.Canvas) onDraw()}, +{@link android.view.View#dispatchDraw(android.graphics.Canvas) dispatchDraw()}, +and the various <code>draw ()methods</code> belonging to the subclasses of the +{@link android.graphics.drawable.Drawable} class. +</p> + +<h4>When this segment is large</h4> + +<p> +In simplified terms, you can understand this metric as showing how long it took +to run all of the calls to +{@link android.view.View#onDraw(android.graphics.Canvas) onDraw()} +for each invalidated view. This +measurement includes any time spent dispatching draw commands to children and +drawables that may be present. For this reason, when you see this bar spike, the +cause could be that a bunch of views suddenly became invalidated. Invalidation +makes it necessary to regenerate views' display lists. Alternatively, a +lengthy time may be the result of a few custom views that have some extremely +complex logic in their +{@link android.view.View#onDraw(android.graphics.Canvas) onDraw()} methods. +</p> + +<h3 id="su">Sync/Upload</h3> + +<p> +The Sync & Upload metric represents the time it takes to transfer +bitmap objects from CPU memory to GPU memory during the current frame. +</p> + +<p> +As different processors, the CPU and the GPU have different RAM areas +dedicated to processing. When you draw a bitmap on Android, the system +transfers the bitmap to GPU memory before the GPU can render it to the +screen. Then, the GPU caches the bitmap so that the system doesn’t need to +transfer the data again unless the texture gets evicted from the GPU texture +cache. +</p> + +<p class="note"><strong>Note:</strong> On Lollipop devices, this stage is +purple. +</p> + +<h4>When this segment is large</h4> + +<p> +All resources for a frame need to reside in GPU memory before they can be +used to draw a frame. This means that a high value for this metric could mean +either a large number of small resource loads or a small number of very large +resources. A common case is when an app displays a single bitmap that’s +close to the size of the screen. Another case is when an app displays a +large number of thumbnails. +</p> + +<p> +To shrink this bar, you can employ techniques such as: +</p> + +<ul> + <li> +Ensuring your bitmap resolutions are not much larger than the size at which they +will be displayed. For example, your app should avoid displaying a 1024x1024 +image as a 48x48 image. + </li> + + <li> +Taking advantage of {@link android.graphics.Bitmap#prepareToDraw()} +to asynchronously pre-upload a bitmap before the next sync phase. + </li> +</ul> + +<h3 id="ic">Issuing Commands</h3> + +<p> +The <em>Issue Commands</em> segment represents the time it takes to issue all +of the commands necessary for drawing display lists to the screen. +</p> + +<p> +For the system to draw display lists to the screen, it sends the +necessary commands to the GPU. Typically, it performs this action through the +<a href="/guide/topics/graphics/opengl.html">OpenGL ES</a> API. +</p> + +<p> +This process takes some time, as the system performs final transformation +and clipping for each command before sending the command to the GPU. Additional +overhead then arises on the GPU side, which computes the final commands. These +commands include final transformations, and additional clipping. +</p> + +<h4>When this segment is large</h4> + +<p> +The time spent in this stage is a direct measure of the complexity and +quantity of display lists that the system renders in a given +frame. For example, having many draw operations, especially in cases where +there's a small inherent cost to each draw primitive, could inflate this time. +For example: +</p> + +<pre> +for (int i = 0; i < 1000; i++) +canvas.drawPoint() +</pre> + +<p> +is a lot more expensive to issue than: +</p> + +<pre> +canvas.drawPoints(mThousandPointArray); +</pre> + +<p> +There isn’t always a 1:1 correlation between issuing commands and +actually drawing display lists. Unlike <em>Issue Commands</em>, +which captures the time it takes to send drawing commands to the GPU, +the <em>Draw</em> metric represents the time that it took to capture the issued +commands into the display list. +</p> + +<p> +This difference arises because the display lists are cached by +the system wherever possible. As a result, there are situations where a +scroll, transform, or animation requires the system to re-send a display +list, but not have to actually rebuild it—recapture the drawing +commands—from scratch. As a result, you can see a high “Issue +commands” bar without seeing a high <em>Draw commands</em> bar. +</p> + +<h3 id="psb">Processing/Swapping Buffers</h3> + +<p> +Once Android finishes submitting all its display list to the GPU, +the system issues one final command to tell the graphics driver that it's +done with the current frame. At this point, the driver can finally present +the updated image to the screen. +</p> + +<h4>When this segment is large</h4> + +<p> +It’s important to understand that the GPU executes work in parallel with the +CPU. The Android system issues draw commands to the GPU, and then moves on to +the next task. The GPU reads those draw commands from a queue and processes +them. +</p> + +<p> +In situations where the CPU issues commands faster than the GPU +consumes them, the communications queue between the processors can become +full. When this occurs, the CPU blocks, and waits until there is space in the +queue to place the next command. This full-queue state arises often during the +<em>Swap Buffers</em> stage, because at that point, a whole frame’s worth of +commands have been submitted. +</p> + +</p> +The key to mitigating this problem is to reduce the complexity of work occurring +on the GPU, in similar fashion to what you would do for the “Issue Commands” +phase. +</p> + + +<h3 id="mt">Miscellaneous</h3> + +<p> +In addition to the time it takes the rendering system to perform its work, +there’s an additional set of work that occurs on the main thread and has +nothing to do with rendering. Time that this work consumes is reported as +<em>misc time</em>. Misc time generally represents work that might be occurring +on the UI thread between two consecutive frames of rendering. +</p> + +<h4>When this segment is large</h4> + +<p> +If this value is high, it is likely that your app has callbacks, intents, or +other work that should be happening on another thread. Tools such as +<a href="/studio/profile/traceview.html">Method +Tracing</a> or <a href="/studio/profile/systrace.html">Systrace</a> can provide +visibility into the tasks that are running on +the main thread. This information can help you target performance improvements. +</p> diff --git a/docs/html/training/_book.yaml b/docs/html/training/_book.yaml index e9635be74ec3..47862e240254 100644 --- a/docs/html/training/_book.yaml +++ b/docs/html/training/_book.yaml @@ -438,16 +438,6 @@ toc: path: /training/efficient-downloads/redundant_redundant.html - title: Modifying Patterns Based on the Connectivity Type path: /training/efficient-downloads/connectivity_patterns.html - - title: Backing up App Data to the Cloud - path: /training/backup/index.html - path_attributes: - - name: description - value: How to sync and back up app and user data to remote web services in the cloud and how to restore the data back to multiple devices. - section: - - title: Configuring Auto Backup - path: /training/backup/autosyncapi.html - - title: Using the Backup API - path: /training/backup/backupapi.html - title: Resolving Cloud Save Conflicts path: /training/cloudsave/conflict-res.html path_attributes: @@ -1156,7 +1146,7 @@ toc: value: 维护兼容性 - name: zh-tw-lang value: 維持相容性 - - title: Selecting Colors with the Palette API + - title: Selecting Colors with the Palette API path: /training/material/palette-colors.html - title: Best Practices for User Input @@ -1242,15 +1232,9 @@ toc: path: /training/scheduling/wakelock.html - title: Scheduling Repeating Alarms path: /training/scheduling/alarms.html - - title: Best Practices for Performance path: /training/best-performance.html section: - - title: Managing Your App's Memory - path: /training/articles/memory.html - path_attributes: - - name: description - value: How to keep your app's memory footprint small in order to improve performance on a variety of mobile devices. - title: Performance Tips path: /training/articles/perf-tips.html path_attributes: @@ -1282,23 +1266,6 @@ toc: - name: description value: How to minimize the amount of power your app requires by adapting to current power conditions and performing power-hungry tasks at proper intervals. section: - - title: Reducing Network Battery Drain - path: /training/performance/battery/network/index.html - section: - - title: Collecting Network Traffic Data - path: /training/performance/battery/network/gather-data.html - - title: Analyzing Network Traffic Data - path: /training/performance/battery/network/analyze-data.html - - title: Optimizing User-Initiated Network Use - path: /training/performance/battery/network/action-user-traffic.html - - title: Optimizing App-Initiated Network Use - path: /training/performance/battery/network/action-app-traffic.html - - title: Optimizing Server-Initiated Network Use - path: /training/performance/battery/network/action-server-traffic.html - - title: Optimizing General Network Use - path: /training/performance/battery/network/action-any-traffic.html - - title: Optimizing for Doze and App Standby - path: /training/monitoring-device-state/doze-standby.html - title: Monitoring the Battery Level and Charging State path: /training/monitoring-device-state/battery-monitoring.html path_attributes: diff --git a/docs/html/training/articles/memory.jd b/docs/html/training/articles/memory.jd deleted file mode 100644 index de7af589aefd..000000000000 --- a/docs/html/training/articles/memory.jd +++ /dev/null @@ -1,740 +0,0 @@ -page.title=Managing Your App's Memory -page.tags=ram,low memory,OutOfMemoryError,onTrimMemory -page.article=true -@jd:body - - -<div id="tb-wrapper"> -<div id="tb"> - -<h2>In this document</h2> -<ol class="nolist"> - <li><a href="#Android">How Android Manages Memory</a> - <ol> - <li><a href="#SharingRAM">Sharing Memory</a></li> - <li><a href="#AllocatingRAM">Allocating and Reclaiming App Memory</a></li> - <li><a href="#RestrictingMemory">Restricting App Memory</a></li> - <li><a href="#SwitchingApps">Switching Apps</a></li> - </ol> - </li> - <li><a href="#YourApp">How Your App Should Manage Memory</a> - <ol> - <li><a href="#Services">Use services sparingly</a></li> - <li><a href="#ReleaseMemoryAsUiGone">Release memory when your user interface becomes hidden</a></li> - <li><a href="#ReleaseMemoryAsTight">Release memory as memory becomes tight</a></li> - <li><a href="#CheckHowMuchMemory">Check how much memory you should use</a></li> - <li><a href="#Bitmaps">Avoid wasting memory with bitmaps</a></li> - <li><a href="#DataContainers">Use optimized data containers</a></li> - <li><a href="#Overhead">Be aware of memory overhead</a></li> - <li><a href="#Abstractions">Be careful with code abstractions</a></li> - <li><a href="#NanoProto">Use nano protobufs for serialized data</a></li> - <li><a href="#DependencyInjection">Avoid dependency injection frameworks</a></li> - <li><a href="#ExternalLibs">Be careful about using external libraries</a></li> - <li><a href="#OverallPerf">Optimize overall performance</a></li> - <li><a href="#Proguard">Use ProGuard to strip out any unneeded code</a></li> - <li><a href="#Zipalign">Use zipalign on your final APK</a></li> - <li><a href="#AnalyzeRam">Analyze your RAM usage</a></li> - <li><a href="#MultipleProcesses">Use multiple processes</a></li> - </ol> - </li> -</ol> -<h2>See Also</h2> -<ul> - <li><a href="{@docRoot}tools/debugging/debugging-memory.html">Investigating Your RAM Usage</a> - </li> -</ul> - -</div> -</div> - - -<p>Random-access memory (RAM) is a valuable resource in any software development environment, but -it's even more valuable on a mobile operating system where physical memory is often constrained. -Although Android's Dalvik virtual machine performs routine garbage collection, this doesn't allow -you to ignore when and where your app allocates and releases memory.</p> - -<p>In order for the garbage collector to reclaim memory from your app, you need to avoid -introducing memory leaks (usually caused by holding onto object references in global members) and -release any {@link java.lang.ref.Reference} objects at the appropriate time (as defined by -lifecycle callbacks discussed further below). For most apps, the Dalvik garbage collector takes -care of the rest: the system reclaims your memory allocations when the corresponding objects leave -the scope of your app's active threads.</p> - -<p>This document explains how Android manages app processes and memory allocation, and how you can -proactively reduce memory usage while developing for Android. For more information about general -practices to clean up your resources when programming in Java, refer to other books or online -documentation about managing resource references. If you’re looking for information about how to -analyze your app’s memory once you’ve already built it, read <a -href="{@docRoot}tools/debugging/debugging-memory.html">Investigating Your RAM Usage</a>.</p> - - - - -<h2 id="Android">How Android Manages Memory</h2> - -<p>Android does not offer swap space for memory, but it does use <a href= -"http://en.wikipedia.org/wiki/Paging" class="external-link">paging</a> and <a href= -"http://en.wikipedia.org/wiki/Memory-mapped_files" class="external-link">memory-mapping</a> -(mmapping) to manage memory. This means that any memory you modify—whether by allocating -new objects or touching mmapped pages—remains resident in RAM and cannot be paged out. -So the only way to completely release memory from your app is to release object references you may -be holding, making the memory available to the garbage collector. That is with one exception: -any files mmapped in without modification, such as code, can be paged out of RAM if the system -wants to use that memory elsewhere.</p> - - -<h3 id="SharingRAM">Sharing Memory</h3> - -<p>In order to fit everything it needs in RAM, Android tries to share RAM pages across processes. It -can do so in the following ways:</p> -<ul> -<li>Each app process is forked from an existing process called Zygote. -The Zygote process starts when the system boots and loads common framework code and resources -(such as activity themes). To start a new app process, the system forks the Zygote process then -loads and runs the app's code in the new process. This allows most of the RAM pages allocated for -framework code and resources to be shared across all app processes.</li> - -<li>Most static data is mmapped into a process. This not only allows that same data to be shared -between processes but also allows it to be paged out when needed. Example static data include: -Dalvik code (by placing it in a pre-linked {@code .odex} file for direct mmapping), app resources -(by designing the resource table to be a structure that can be mmapped and by aligning the zip -entries of the APK), and traditional project elements like native code in {@code .so} files.</li> - -<li>In many places, Android shares the same dynamic RAM across processes using explicitly allocated -shared memory regions (either with ashmem or gralloc). For example, window surfaces use shared -memory between the app and screen compositor, and cursor buffers use shared memory between the -content provider and client.</li> -</ul> - -<p>Due to the extensive use of shared memory, determining how much memory your app is using requires -care. Techniques to properly determine your app's memory use are discussed in <a -href="{@docRoot}tools/debugging/debugging-memory.html">Investigating Your RAM Usage</a>.</p> - - -<h3 id="AllocatingRAM">Allocating and Reclaiming App Memory</h3> - -<p>Here are some facts about how Android allocates then reclaims memory from your app:</p> - -<ul> -<li>The Dalvik heap for each process is constrained to a single virtual memory range. This defines -the logical heap size, which can grow as it needs to (but only up to a limit that the system defines -for each app).</li> - -<li>The logical size of the heap is not the same as the amount of physical memory used by the heap. -When inspecting your app's heap, Android computes a value called the Proportional Set Size (PSS), -which accounts for both dirty and clean pages that are shared with other processes—but only in an -amount that's proportional to how many apps share that RAM. This (PSS) total is what the system -considers to be your physical memory footprint. For more information about PSS, see the <a -href="{@docRoot}tools/debugging/debugging-memory.html#ViewingAllocations">Investigating Your -RAM Usage</a> guide.</li> - -<li>The Dalvik heap does not compact the logical size of the heap, meaning that Android does not -defragment the heap to close up space. Android can only shrink the logical heap size when there -is unused space at the end of the heap. But this doesn't mean the physical memory used by the heap -can't shrink. After garbage collection, Dalvik walks the heap and finds unused pages, then returns -those pages to the kernel using madvise. So, paired allocations and deallocations of large -chunks should result in reclaiming all (or nearly all) the physical memory used. However, -reclaiming memory from small allocations can be much less efficient because the page used -for a small allocation may still be shared with something else that has not yet been freed.</li> -</ul> - - -<h3 id="RestrictingMemory">Restricting App Memory</h3> - -<p>To maintain a functional multi-tasking environment, Android sets a hard limit on the heap size -for each app. The exact heap size limit varies between devices based on how much RAM the device -has available overall. If your app has reached the heap capacity and tries to allocate more -memory, it will receive an {@link java.lang.OutOfMemoryError}.</p> - -<p>In some cases, you might want to query the system to determine exactly how much heap space you -have available on the current device—for example, to determine how much data is safe to keep in a -cache. You can query the system for this figure by calling {@link -android.app.ActivityManager#getMemoryClass()}. This returns an integer indicating the number of -megabytes available for your app's heap. This is discussed further below, under -<a href="#CheckHowMuchMemory">Check how much memory you should use</a>.</p> - - -<h3 id="SwitchingApps">Switching Apps</h3> - -<p>Instead of using swap space when the user switches between apps, Android keeps processes that -are not hosting a foreground ("user visible") app component in a least-recently used (LRU) cache. -For example, when the user first launches an app, a process is created for it, but when the user -leaves the app, that process does <em>not</em> quit. The system keeps the process cached, so if -the user later returns to the app, the process is reused for faster app switching.</p> - -<p>If your app has a cached process and it retains memory that it currently does not need, -then your app—even while the user is not using it—is constraining the system's -overall performance. So, as the system runs low on memory, it may kill processes in the LRU cache -beginning with the process least recently used, but also giving some consideration toward -which processes are most memory intensive. To keep your process cached as long as possible, follow -the advice in the following sections about when to release your references.</p> - -<p>More information about how processes are cached while not running in the foreground and how -Android decides which ones -can be killed is available in the <a href="{@docRoot}guide/components/processes-and-threads.html" ->Processes and Threads</a> guide.</p> - - - - -<h2 id="YourApp">How Your App Should Manage Memory</h2> - -<p>You should consider RAM constraints throughout all phases of development, including during app -design (before you begin development). There are many -ways you can design and write code that lead to more efficient results, through aggregation of the -same techniques applied over and over.</p> - -<p>You should apply the following techniques while designing and implementing your app to make it -more memory efficient.</p> - - -<h3 id="Services">Use services sparingly</h3> - -<p>If your app needs a <a href="{@docRoot}guide/components/services.html">service</a> -to perform work in the background, do not keep it running unless -it's actively performing a job. Also be careful to never leak your service by failing to stop it -when its work is done.</p> - -<p>When you start a service, the system prefers to always keep the process for that service -running. This makes the process very expensive because the RAM used by the service can’t be used by -anything else or paged out. This reduces the number of cached processes that the system can keep in -the LRU cache, making app switching less efficient. It can even lead to thrashing in the system -when memory is tight and the system can’t maintain enough processes to host all the services -currently running.</p> - -<p>The best way to limit the lifespan of your service is to use an {@link -android.app.IntentService}, which finishes -itself as soon as it's done handling the intent that started it. For more information, read -<a href="{@docRoot}training/run-background-service/index.html">Running in a Background Service</a> -.</p> - -<p>Leaving a service running when it’s not needed is <strong>one of the worst memory-management -mistakes</strong> an Android app can make. So don’t be greedy by keeping a service for your app -running. Not only will it increase the risk of your app performing poorly due to RAM constraints, -but users will discover such misbehaving apps and uninstall them.</p> - - -<h3 id="ReleaseMemoryAsUiGone">Release memory when your user interface becomes hidden</h3> - -<p>When the user navigates to a different app and your UI is no longer visible, you should -release any resources that are used by only your UI. Releasing UI resources at this time can -significantly increase the system's capacity for cached processes, which has a direct impact on the -quality of the user experience.</p> - -<p>To be notified when the user exits your UI, implement the {@link -android.content.ComponentCallbacks2#onTrimMemory onTrimMemory()} callback in your {@link -android.app.Activity} classes. You should use this -method to listen for the {@link android.content.ComponentCallbacks2#TRIM_MEMORY_UI_HIDDEN} level, -which indicates your UI is now hidden from view and you should free resources that only your UI -uses.</p> - - -<p>Notice that your app receives the {@link android.content.ComponentCallbacks2#onTrimMemory -onTrimMemory()} callback with {@link android.content.ComponentCallbacks2#TRIM_MEMORY_UI_HIDDEN} -only when <em>all the UI components</em> of your app process become hidden from the user. -This is distinct -from the {@link android.app.Activity#onStop onStop()} callback, which is called when an {@link -android.app.Activity} instance becomes hidden, which occurs even when the user moves to -another activity in your app. So although you should implement {@link android.app.Activity#onStop -onStop()} to release activity resources such as a network connection or to unregister broadcast -receivers, you usually should not release your UI resources until you receive {@link -android.content.ComponentCallbacks2#onTrimMemory onTrimMemory(TRIM_MEMORY_UI_HIDDEN)}. This ensures -that if the user navigates <em>back</em> from another activity in your app, your UI resources are -still available to resume the activity quickly.</p> - - - -<h3 id="ReleaseMemoryAsTight">Release memory as memory becomes tight</h3> - -<p>During any stage of your app's lifecycle, the {@link -android.content.ComponentCallbacks2#onTrimMemory onTrimMemory()} callback also tells you when -the overall device memory is getting low. You should respond by further releasing resources based -on the following memory levels delivered by {@link android.content.ComponentCallbacks2#onTrimMemory -onTrimMemory()}:</p> - -<ul> -<li>{@link android.content.ComponentCallbacks2#TRIM_MEMORY_RUNNING_MODERATE} -<p>Your app is running and not considered killable, but the device is running low on memory and the -system is actively killing processes in the LRU cache.</p> -</li> - -<li>{@link android.content.ComponentCallbacks2#TRIM_MEMORY_RUNNING_LOW} -<p>Your app is running and not considered killable, but the device is running much lower on -memory so you should release unused resources to improve system performance (which directly -impacts your app's performance).</p> -</li> - -<li>{@link android.content.ComponentCallbacks2#TRIM_MEMORY_RUNNING_CRITICAL} -<p>Your app is still running, but the system has already killed most of the processes in the -LRU cache, so you should release all non-critical resources now. If the system cannot reclaim -sufficient amounts of RAM, it will clear all of the LRU cache and begin killing processes that -the system prefers to keep alive, such as those hosting a running service.</p> -</li> -</ul> - -<p>Also, when your app process is currently cached, you may receive one of the following -levels from {@link android.content.ComponentCallbacks2#onTrimMemory onTrimMemory()}:</p> -<ul> -<li>{@link android.content.ComponentCallbacks2#TRIM_MEMORY_BACKGROUND} -<p>The system is running low on memory and your process is near the beginning of the LRU list. -Although your app process is not at a high risk of being killed, the system may already be killing -processes in the LRU cache. You should release resources that are easy to recover so your process -will remain in the list and resume quickly when the user returns to your app.</p> -</li> - -<li>{@link android.content.ComponentCallbacks2#TRIM_MEMORY_MODERATE} -<p>The system is running low on memory and your process is near the middle of the LRU list. If the -system becomes further constrained for memory, there's a chance your process will be killed.</p> -</li> - -<li>{@link android.content.ComponentCallbacks2#TRIM_MEMORY_COMPLETE} -<p>The system is running low on memory and your process is one of the first to be killed if the -system does not recover memory now. You should release everything that's not critical to -resuming your app state.</p> - -</li> -</ul> - -<p>Because the {@link android.content.ComponentCallbacks2#onTrimMemory onTrimMemory()} callback was -added in API level 14, you can use the {@link android.content.ComponentCallbacks#onLowMemory()} -callback as a fallback for older versions, which is roughly equivalent to the {@link -android.content.ComponentCallbacks2#TRIM_MEMORY_COMPLETE} event.</p> - -<p class="note"><strong>Note:</strong> When the system begins killing processes in the LRU cache, -although it primarily works bottom-up, it does give some consideration to which processes are -consuming more memory and will thus provide the system more memory gain if killed. -So the less memory you consume while in the LRU list overall, the better your chances are -to remain in the list and be able to quickly resume.</p> - - - -<h3 id="CheckHowMuchMemory">Check how much memory you should use</h3> - -<p>As mentioned earlier, each Android-powered device has a different amount of RAM available to the -system and thus provides a different heap limit for each app. You can call {@link -android.app.ActivityManager#getMemoryClass()} to get an estimate of your app's available heap in -megabytes. If your app tries to allocate more memory than is available here, it will receive an -{@link java.lang.OutOfMemoryError}.</p> - -<p>In very special situations, you can request a larger heap size by setting the <a -href="{@docRoot}guide/topics/manifest/application-element.html#largeHeap">{@code largeHeap}</a> -attribute to "true" in the manifest <a -href="{@docRoot}guide/topics/manifest/application-element.html">{@code <application>}</a> -tag. If you do so, you can call {@link -android.app.ActivityManager#getLargeMemoryClass()} to get an estimate of the large heap size.</p> - -<p>However, the ability to request a large heap is intended only for a small set of apps that can -justify the need to consume more RAM (such as a large photo editing app). <strong>Never request a -large heap simply because you've run out of memory</strong> and you need a quick fix—you -should use it only when you know exactly where all your memory is being allocated and why it must -be retained. Yet, even when you're confident your app can justify the large heap, you should avoid -requesting it to whatever extent possible. Using the extra memory will increasingly be to the -detriment of the overall user experience because garbage collection will take longer and system -performance may be slower when task switching or performing other common operations.</p> - -<p>Additionally, the large heap size is not the same on all devices and, when running on -devices that have limited RAM, the large heap size may be exactly the same as the regular heap -size. So even if you do request the large heap size, you should call {@link -android.app.ActivityManager#getMemoryClass()} to check the regular heap size and strive to always -stay below that limit.</p> - - -<h3 id="Bitmaps">Avoid wasting memory with bitmaps</h3> - -<p>When you load a bitmap, keep it in RAM only at the resolution you need for the current device's -screen, scaling it down if the original bitmap is a higher resolution. Keep in mind that an -increase in bitmap resolution results in a corresponding (increase<sup>2</sup>) in memory needed, -because both the X and Y dimensions increase.</p> - -<p class="note"><strong>Note:</strong> On Android 2.3.x (API level 10) and below, bitmap objects -always appear as the same size in your app heap regardless of the image resolution (the actual -pixel data is stored separately in native memory). This makes it more difficult to debug the bitmap -memory allocation because most heap analysis tools do not see the native allocation. However, -beginning in Android 3.0 (API level 11), the bitmap pixel data is allocated in your app's Dalvik -heap, improving garbage collection and debuggability. So if your app uses bitmaps and you're having -trouble discovering why your app is using some memory on an older device, switch to a device -running Android 3.0 or higher to debug it.</p> - -<p>For more tips about working with bitmaps, read <a -href="{@docRoot}training/displaying-bitmaps/manage-memory.html">Managing Bitmap Memory</a>.</p> - - -<h3 id="DataContainers">Use optimized data containers</h3> - -<p>Take advantage of optimized containers in the Android framework, such as {@link -android.util.SparseArray}, {@link android.util.SparseBooleanArray}, and {@link -android.support.v4.util.LongSparseArray}. The generic {@link java.util.HashMap} -implementation can be quite memory -inefficient because it needs a separate entry object for every mapping. Additionally, the {@link -android.util.SparseArray} classes are more efficient because they avoid the system's need -to <acronym title= -"Automatic conversion from primitive types to object classes (such as int to Integer)" ->autobox</acronym> -the key and sometimes value (which creates yet another object or two per entry). And don't be -afraid of dropping down to raw arrays when that makes sense.</p> - - - -<h3 id="Overhead">Be aware of memory overhead</h3> - -<p>Be knowledgeable about the cost and overhead of the language and libraries you are using, and -keep this information in mind when you design your app, from start to finish. Often, things on the -surface that look innocuous may in fact have a large amount of overhead. Examples include:</p> -<ul> -<li>Enums often require more than twice as much memory as static constants. You should strictly -avoid using enums on Android.</li> - -<li>Every class in Java (including anonymous inner classes) uses about 500 bytes of code.</li> - -<li>Every class instance has 12-16 bytes of RAM overhead.</li> - -<li>Putting a single entry into a {@link java.util.HashMap} requires the allocation of an -additional entry object that takes 32 bytes (see the previous section about <a -href="#DataContainers">optimized data containers</a>).</li> -</ul> - -<p>A few bytes here and there quickly add up—app designs that are class- or object-heavy will suffer -from this overhead. That can leave you in the difficult position of looking at a heap analysis and -realizing your problem is a lot of small objects using up your RAM.</p> - - -<h3 id="Abstractions">Be careful with code abstractions</h3> - -<p>Often, developers use abstractions simply as a "good programming practice," because abstractions -can improve code flexibility and maintenance. However, abstractions come at a significant cost: -generally they require a fair amount more code that needs to be executed, requiring more time and -more RAM for that code to be mapped into memory. So if your abstractions aren't supplying a -significant benefit, you should avoid them.</p> - - -<h3 id="NanoProto">Use nano protobufs for serialized data</h3> - -<p><a href="https://developers.google.com/protocol-buffers/docs/overview">Protocol -buffers</a> are a language-neutral, platform-neutral, extensible mechanism designed by Google for -serializing structured data—think XML, but smaller, faster, and simpler. If you decide to use -protobufs for your data, you should always use nano protobufs in your client-side code. Regular -protobufs generate extremely verbose code, which will cause many kinds of problems in your app: -increased RAM use, significant APK size increase, slower execution, and quickly hitting the DEX -symbol limit.</p> - -<p>For more information, see the "Nano version" section in the <a -href="https://android.googlesource.com/platform/external/protobuf/+/master/java/README.txt" -class="external-link">protobuf readme</a>.</p> - - - -<h3 id="DependencyInjection">Avoid dependency injection frameworks</h3> - -<p>Using a dependency injection framework such as <a -href="https://code.google.com/p/google-guice/" class="external-link">Guice</a> or -<a href="https://github.com/roboguice/roboguice" class="external-link">RoboGuice</a> may be -attractive because they can simplify the code you write and provide an adaptive environment -that's useful for testing and other configuration changes. However, these frameworks tend to perform -a lot of process initialization by scanning your code for annotations, which can require significant -amounts of your code to be mapped into RAM even though you don't need it. These mapped pages are -allocated into clean memory so Android can drop them, but that won't happen until the pages have -been left in memory for a long period of time.</p> - - -<h3 id="ExternalLibs">Be careful about using external libraries</h3> - -<p>External library code is often not written for mobile environments and can be inefficient when used -for work on a mobile client. At the very least, when you decide to use an external library, you -should assume you are taking on a significant porting and maintenance burden to optimize the -library for mobile. Plan for that work up-front and analyze the library in terms of code size and -RAM footprint before deciding to use it at all.</p> - -<p>Even libraries supposedly designed for use on Android are potentially dangerous because each -library may do things differently. For example, one library may use nano protobufs while another -uses micro protobufs. Now you have two different protobuf implementations in your app. This can and -will also happen with different implementations of logging, analytics, image loading frameworks, -caching, and all kinds of other things you don't expect. <a -href="{@docRoot}tools/help/proguard.html">ProGuard</a> won't save you here because these -will all be lower-level dependencies that are required by the features for which you want the -library. This becomes especially problematic when you use an {@link android.app.Activity} -subclass from a library (which -will tend to have wide swaths of dependencies), when libraries use reflection (which is common and -means you need to spend a lot of time manually tweaking ProGuard to get it to work), and so on.</p> - -<p>Also be careful not to fall into the trap of using a shared library for one or two features out of -dozens of other things it does; you don't want to pull in a large amount of code and overhead that -you don't even use. At the end of the day, if there isn't an existing implementation that is a -strong match for what you need to do, it may be best if you create your own implementation.</p> - - -<h3 id="OverallPerf">Optimize overall performance</h3> - -<p>A variety of information about optimizing your app's overall performance is available -in other documents listed in <a href="{@docRoot}training/best-performance.html">Best Practices -for Performance</a>. Many of these documents include optimizations tips for CPU performance, but -many of these tips also help optimize your app's memory use, such as by reducing the number of -layout objects required by your UI.</p> - -<p>You should also read about <a href="{@docRoot}tools/debugging/debugging-ui.html">optimizing -your UI</a> with the layout debugging tools and take advantage of -the optimization suggestions provided by the <a -href="{@docRoot}tools/debugging/improving-w-lint.html">lint tool</a>.</p> - - -<h3 id="Proguard">Use ProGuard to strip out any unneeded code</h3> - -<p>The <a href="{@docRoot}tools/help/proguard.html">ProGuard</a> tool shrinks, -optimizes, and obfuscates your code by removing unused code and renaming classes, fields, and -methods with semantically obscure names. Using ProGuard can make your code more compact, requiring -fewer RAM pages to be mapped.</p> - - -<h3 id="Zipalign">Use zipalign on your final APK</h3> - -<p>If you do any post-processing of an APK generated by a build system (including signing it -with your final production certificate), then you must run <a -href="{@docRoot}tools/help/zipalign.html">zipalign</a> on it to have it re-aligned. -Failing to do so can cause your app to require significantly more RAM, because things like -resources can no longer be mmapped from the APK.</p> - -<p class="note"><strong>Note:</strong> Google Play Store does not accept APK files that -are not zipaligned.</p> - - -<h3 id="AnalyzeRam">Analyze your RAM usage</h3> - -<p>Once you achieve a relatively stable build, begin analyzing how much RAM your app is using -throughout all stages of its lifecycle. For information about how to analyze your app, read <a -href="{@docRoot}tools/debugging/debugging-memory.html">Investigating Your RAM Usage</a>.</p> - - - - -<h3 id="MultipleProcesses">Use multiple processes</h3> - -<p>If it's appropriate for your app, an advanced technique that may help you manage your app's -memory is dividing components of your app into multiple processes. This technique must always be -used carefully and <strong>most apps should not run multiple processes</strong>, as it can easily -increase—rather than decrease—your RAM footprint if done incorrectly. It is primarily -useful to apps that may run significant work in the background as well as the foreground and can -manage those operations separately.</p> - - -<p>An example of when multiple processes may be appropriate is when building a music player that -plays music from a service for long period of time. If -the entire app runs in one process, then many of the allocations performed for its activity UI must -be kept around as long as it is playing music, even if the user is currently in another app and the -service is controlling the playback. An app like this may be split into two process: one for its -UI, and the other for the work that continues running in the background service.</p> - -<p>You can specify a separate process for each app component by declaring the <a href= -"{@docRoot}guide/topics/manifest/service-element.html#proc">{@code android:process}</a> attribute -for each component in the manifest file. For example, you can specify that your service should run -in a process separate from your app's main process by declaring a new process named "background" -(but you can name the process anything you like):</p> - -<pre> -<service android:name=".PlaybackService" - android:process=":background" /> -</pre> - -<p>Your process name should begin with a colon (':') to ensure that the process remains private to -your app.</p> - -<p>Before you decide to create a new process, you need to understand the memory implications. -To illustrate the consequences of each process, consider that an empty process doing basically -nothing has an extra memory footprint of about 1.4MB, as shown by the memory information -dump below.</p> - -<pre class="no-pretty-print"> -adb shell dumpsys meminfo com.example.android.apis:empty - -** MEMINFO in pid 10172 [com.example.android.apis:empty] ** - Pss Pss Shared Private Shared Private Heap Heap Heap - Total Clean Dirty Dirty Clean Clean Size Alloc Free - ------ ------ ------ ------ ------ ------ ------ ------ ------ - Native Heap 0 0 0 0 0 0 1864 1800 63 - Dalvik Heap 764 0 5228 316 0 0 5584 5499 85 - Dalvik Other 619 0 3784 448 0 0 - Stack 28 0 8 28 0 0 - Other dev 4 0 12 0 0 4 - .so mmap 287 0 2840 212 972 0 - .apk mmap 54 0 0 0 136 0 - .dex mmap 250 148 0 0 3704 148 - Other mmap 8 0 8 8 20 0 - Unknown 403 0 600 380 0 0 - TOTAL 2417 148 12480 1392 4832 152 7448 7299 148 -</pre> - -<p class="note"><strong>Note:</strong> More information about how to read this output is provided -in <a href="{@docRoot}tools/debugging/debugging-memory.html#ViewingAllocations">Investigating -Your RAM Usage</a>. The key data here is the <em>Private Dirty</em> and <em>Private -Clean</em> memory, which shows that this process is using almost 1.4MB of non-pageable RAM -(distributed across the Dalvik heap, native allocations, book-keeping, and library-loading), -and another 150K of RAM for code that has been mapped in to execute.</p> - -<p>This memory footprint for an empty process is fairly significant and it can quickly -grow as you start doing work in that process. For -example, here is the memory use of a process that is created only to show an activity with some -text in it:</p> - -<pre class="no-pretty-print"> -** MEMINFO in pid 10226 [com.example.android.helloactivity] ** - Pss Pss Shared Private Shared Private Heap Heap Heap - Total Clean Dirty Dirty Clean Clean Size Alloc Free - ------ ------ ------ ------ ------ ------ ------ ------ ------ - Native Heap 0 0 0 0 0 0 3000 2951 48 - Dalvik Heap 1074 0 4928 776 0 0 5744 5658 86 - Dalvik Other 802 0 3612 664 0 0 - Stack 28 0 8 28 0 0 - Ashmem 6 0 16 0 0 0 - Other dev 108 0 24 104 0 4 - .so mmap 2166 0 2824 1828 3756 0 - .apk mmap 48 0 0 0 632 0 - .ttf mmap 3 0 0 0 24 0 - .dex mmap 292 4 0 0 5672 4 - Other mmap 10 0 8 8 68 0 - Unknown 632 0 412 624 0 0 - TOTAL 5169 4 11832 4032 10152 8 8744 8609 134 -</pre> - -<p>The process has now almost tripled in size, to 4MB, simply by showing some text in the UI. This -leads to an important conclusion: If you are going to split your app into multiple processes, only -one process should be responsible for UI. Other processes should avoid any UI, as this will quickly -increase the RAM required by the process (especially once you start loading bitmap assets and other -resources). It may then be hard or impossible to reduce the memory usage once the UI is drawn.</p> - -<p>Additionally, when running more than one process, it's more important than ever that you keep your -code as lean as possible, because any unnecessary RAM overhead for common implementations are now -replicated in each process. For example, if you are using enums (though <a -href="#Overhead">you should not use enums</a>), all of -the RAM needed to create and initialize those constants is duplicated in each process, and any -abstractions you have with adapters and temporaries or other overhead will likewise be replicated.</p> - -<p>Another concern with multiple processes is the dependencies that exist between them. For example, -if your app has a content provider that you have running in the default process which also hosts -your UI, then code in a background process that uses that content provider will also require that -your UI process remain in RAM. If your goal is to have a background process that can run -independently of a heavy-weight UI process, it can't have dependencies on content providers or -services that execute in the UI process.</p> - - - - - - - - - - -<!-- THE FOLLOWING IS OVERWHELMING AND NOT NECESSARY FOR MOST APPS, LEAVING OUT FOR NOW - - -<p>You can examine the dependencies between your processes with the command:</p> - -<pre class="no-pretty-print"> -adb shell dumpsys activity -</pre> - -<p>This dumps various information about the Activity Manager's state, ending with a list of all -processes in their memory management order, including the reason each process is at its given -level. For example, below is a dump with the Music app in the foreground.</p> - -<pre class="no-pretty-print"> -ACTIVITY MANAGER RUNNING PROCESSES (dumpsys activity processes) - Process LRU list (sorted by oom_adj): - PERS # 4: adj=sys /F trm= 0 20674:system/1000 (fixed) - PERS #39: adj=pers /F trm= 0 20964:com.android.nfc/1027 (fixed) - PERS # 2: adj=pers /F trm= 0 20959:com.android.phone/1001 (fixed) - PERS # 1: adj=pers /F trm= 0 20779:com.android.systemui/u0a10057 (fixed) - Proc #11: adj=fore /FA trm= 0 8663:com.google.android.music:ui/u0a10043 (top-activity) - Proc #10: adj=fore /F trm= 0 30881:com.google.android.music:main/u0a10043 (provider) - com.google.android.music/.store.MusicContentProvider<=Proc{8663:com.google.android.music:ui/u0a10043} - Proc # 6: adj=fore /F trm= 0 21014:com.google.process.gapps/u0a10023 (provider) - com.google.android.gsf/.settings.GoogleSettingsProvider<=Proc{20935:com.google.process.location/u0a10023} - Proc #38: adj=vis /F trm= 0 21028:com.android.nfc:handover/1027 (service) - com.android.nfc/.handover.HandoverService<=Proc{20964:com.android.nfc/1027} - Proc # 7: adj=vis /B trm= 0 20935:com.google.process.location/u0a10023 (service) - com.google.android.location/.GeocodeService<=Proc{20674:system/1000} - Proc # 3: adj=vis /F trm= 0 21225:com.android.bluetooth/1002 (service) - com.android.bluetooth/.hfp.HeadsetService<=Proc{20674:system/1000} - Proc # 0: adj=vis /F trm= 0 20908:com.google.android.inputmethod.latin/u0a10035 (service) - com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME<=Proc{20674:system/1000} - Proc #34: adj=svc /B trm= 0 16765:com.google.android.apps.currents/u0a10012 (started-services) - Proc #14: adj=svc /B trm= 0 21148:com.google.android.gms/u0a10023 (started-services) - Proc #12: adj=home /B trm= 0 20989:com.android.launcher/u0a10036 (home) - Proc #37: adj=svcb /B trm= 0 15194:com.google.android.apps.googlevoice/u0a10089 (started-services) - Proc #17: adj=svcb /B trm= 0 24537:android.process.media/u0a10016 (started-services) - Proc #35: adj=bak /B trm= 0 16087:com.android.defcontainer/u0a10013 (service) - com.android.defcontainer/.DefaultContainerService<=Proc{16050:com.android.settings/1000} - Proc #16: adj=bak /B trm= 0 7334:com.google.android.gm/u0a10022 (bg-act) - Proc #15: adj=bak /B trm= 0 22499:com.google.android.googlequicksearchbox/u0a10060 (bg-act) - Proc # 9: adj=bak /B trm= 0 20856:com.google.android.gsf.login/u0a10023 (bg-empty) - Proc #26: adj=bak+1/B trm= 0 9923:com.android.mms/u0a10042 (bg-act) - Proc #23: adj=bak+1/B trm= 0 16721:com.android.chrome/u0a10010 (bg-act) - Proc #22: adj=bak+1/B trm= 0 17596:com.android.chrome:sandboxed_process0/u0a10010i33 (service) - com.android.chrome/org.chromium.content.app.SandboxedProcessService0<=Proc{16721:com.android.chrome/u0a10010} - Proc #19: adj=bak+1/B trm= 0 17442:com.google.android.youtube/u0a10067 (bg-services) - Proc #18: adj=bak+2/B trm= 0 16740:com.google.android.apps.plus/u0a10052 (bg-empty) - Proc #13: adj=bak+2/B trm= 0 7707:com.android.musicfx/u0a10044 (bg-empty) - Proc #36: adj=bak+3/B trm= 0 16050:com.android.settings/1000 (bg-act) - Proc #33: adj=bak+3/B trm= 0 16863:com.android.dialer/u0a10015 (bg-act) -</pre> - - -<p class="note"><strong>Note:</strong> The exact details of what is shown here will vary across -platform versions as process management policies are tweaked and improved.</p> - - -<p>Details on the highlighted sections are:</p> - -<ol> -<li>Foreground app: This is the current app running in the foreground -- it is in the "fore" memory -class because it is the top activity on the activity stack.</li> - -<li>Persistent processes: These are processes that are part of the core system that must always be -running.</li> - -<li>Dependent process: This shows how the Music app is using two processes. Its UI process has a -dependency on the "main" process (through a content provider). So while the UI process is in use, -the main process must also be kept around. This means the app's memory footprint is actually the -sum of both processes. You will have this kind of connection on a content provider any time you -have active calls into it or have unclosed cursors or file streams that came from it.</li> - -<li>Visible processes: These are processes that count in some way as "visible" to the user. This -generally means that it is either something the user can literally see (such as a process hosting a -paused but visible activity that is behind a non-full-screen dialog) or is something the user might -notice if the process disappeared (such as a foreground service playing music). You should be -certain that any process you have running at the "visible" level is indeed critical to the user, -because they are very expensive to the overall RAM load.</li> - -<li>Service processes: These are processes running long-term jobs in a service. This level of the -list is the start of less-critical processes, which the system has some freedom to kill if RAM is -needed elsewhere. These services are still quite expensive because they can be killed only -temporarily and the system tries to keep them running whenever possible.</li> - -<li>Home process: A special slot for the process that hosts the current Home activity, to try to -prevent it from being killed as much as possible. Killing this process is much more damaging to the -user experience than killing other cached processes, because so much user interaction goes through -home.</li> - -<li>Secondary service processes: These are services that have been running for a relatively long time -and so should be killed more aggressively when RAM is needed elsewhere.</li> - -<li>Cached processes: These are cached processes held in the LRU cache, which allow for fast app -switching and component launching. These processes are not required and the system will kill them -as needed to reclaim memory. You will often see a process hosting a running service here—this is -part of a platform policy of allowing very long-running services to drop down into the LRU list and -eventually be killed. If the service should continue running (as defined by the {@link -android.app.Service#onStartCommand onStartCommand()} return value, such as {@link -android.app.Service#START_STICKY}), the the system eventually restarts it. This avoids issues with -such services having memory leaks that over time reduce the number of regular cached processes that -can be kept.</li> - -</ol> - -<p>This numbered list of processes is essentially the LRU list of processes that the framework -provides to the kernel to help it determine which processes it should kill as it needs more RAM. -The kernel's out of memory killer will generally begin from the bottom of this list, killing the -last process and working its way up. It may not do it in exactly this order, as it can also take -into consideration other factors such as the relative RAM footprint of processes to some degree.</p> - -<p>There are many other options you can use with the activity command to analyze further details of -your app's state—use <code>adb shell dumpsys activity -h</code> for help on its use.</p> - ---> diff --git a/docs/html/training/articles/perf-tips.jd b/docs/html/training/articles/perf-tips.jd index 82de69a55249..30cab14d5da6 100644 --- a/docs/html/training/articles/perf-tips.jd +++ b/docs/html/training/articles/perf-tips.jd @@ -28,7 +28,8 @@ when combined, but it's unlikely that these changes will result in dramatic performance effects. Choosing the right algorithms and data structures should always be your priority, but is outside the scope of this document. You should use the tips in this document as general coding practices that you can incorporate into your habits for general code -efficiency.</p> +efficiency. +</p> <p>There are two basic rules for writing efficient code:</p> <ul> @@ -49,8 +50,7 @@ code for a device with a JIT is not always the best code for a device without.</p> <p>To ensure your app performs well across a wide variety of devices, ensure -your code is efficient at all levels and agressively optimize your performance.</p> - +your code is efficient at all levels and aggressively optimize your performance.</p> <h2 id="ObjectCreation">Avoid Creating Unnecessary Objects</h2> diff --git a/docs/html/training/auto/audio/index.jd b/docs/html/training/auto/audio/index.jd index 3a1b1e883494..65883671f956 100644 --- a/docs/html/training/auto/audio/index.jd +++ b/docs/html/training/auto/audio/index.jd @@ -596,7 +596,7 @@ href="https://www.youtube.com/watch?v=xc2HZSwPcwM"> </a> <h2 id="support_voice">Support Voice Actions</h2> -<p>To reduce driver distractions, you can add voice actions in your audio playback app. With voice +<p>To reduce driver distractions, you must add voice actions in your audio playback app. With voice action support, users can launch your app and play audio by providing voice input on Auto screens. If your audio playback app is already active and the user says <i>“Play a song”</i>, the system starts playing music without requiring the user to look at or touch diff --git a/docs/html/training/backup/autosyncapi.jd b/docs/html/training/backup/autosyncapi.jd deleted file mode 100644 index e0df7bb658ae..000000000000 --- a/docs/html/training/backup/autosyncapi.jd +++ /dev/null @@ -1,370 +0,0 @@ -page.title=Configuring Auto Backup for Apps -page.tags=backup, marshmallow, androidm -page.keywords=backup, autobackup -page.image=images/cards/card-auto-backup_2x.png - -@jd:body - -<div id="tb-wrapper"> -<div id="tb"> -<h2>This lesson teaches you to</h2> -<ol> - <li><a href="#configuring">Configure Data Backup</a></li> - <li><a href="#previous-androids">Support Lower Versions of Android</a></li> - <li><a href="#testing">Test Backup Configuration</a></li> - <li><a href="#issues">Handle Google Cloud Messaging</a></li> -</ol> - <h2>You should also read</h2> - <ul> - <li><a href="{@docRoot}guide/topics/data/backup.html">Data Backup</a></li> - <li><a href="{@docRoot}training/backup/backupapi.html">Using the Backup API</a> - </li> - </ul> - -</div> -</div> - -<p> - Users frequently invest time and effort to configure apps just the way they like them. Switching - to a new device can cancel out all that careful configuration. For apps whose <a href= - "{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">target SDK version</a> - is Android 6.0 (API level 23) and higher, devices running Android 6.0 and higher automatically - back up app data to the cloud. The system performs this automatic backup - for nearly all app data by default, and does so without your having to write any additional app - code. -</p> - -<p class="note"> -<strong>Note:</strong> To protect user privacy, the device user must have opted in to Google -services for Auto Backup to work. The Google services opt-in dialog appears when the user goes -through the Setup Wizard or configures the first Google account on the device. -</p> - -<p> - When a user installs your app on - a new device, or reinstalls your app on one (for example, after a factory reset), the system - automatically restores the app data from the cloud. This lesson provides information about how to - configure the Auto Backup for Apps feature, explaining its default behavior and how to - exclude data that you don't want the system to back up. -</p> - -<p> - The automatic backup feature preserves the data your app creates on a user device by uploading it - to the user’s Google Drive account and encrypting it. There is no charge to you or the user for - data storage, and the saved data does not count towards the user's personal Google Drive quota. - Each app can store up to 25MB. Once its backed-up data reaches 25MB, the app no longer sends - data to the cloud. If the system performs a data restore, it uses the last data snapshot that - the app had sent to the cloud. -</p> - -<p>Automatic backups occur when the following conditions are met:</p> - <ul> - <li>The device is idle.</li> - <li>The device is charging.</li> - <li>The device is connected to a Wi-Fi network.</li> - <li>At least 24 hours have elapsed since the last backup.</li> - </ul> -</p> - -<h2 id="configuring">Configure Data Backup</h2> - -<p> - On devices running Android 6.0 (API level 23) or higher, the default system behavior is to back up - almost all data that an app creates. The exception is <a href="#auto-exclude"> - automatically excluded data files</a>. This section explains how you can use settings in - your app <a href="{@docRoot}guide/topics/manifest/manifest-intro.html">manifest</a> to further - limit and configure what data the system backs up. -</p> - -<h3 id="include-exclude">Including or excluding data</h3> - -<p> - Depending on what data your app needs and how you save it, you may need to set specific - rules for including or excluding certain files or directories. Auto Backup for Apps - lets you set these backup rules through the app manifest, in which you specify a backup scheme - configuration XML file. For example: -</p> - -<pre> -<?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - package="com.my.appexample"> - <uses-sdk android:minSdkVersion="23"/> - <uses-sdk android:targetSdkVersion="23"/> - <application ... -<strong> android:fullBackupContent="@xml/mybackupscheme"></strong> - </app> - ... -</manifest> -</pre> - -<p> - In this example, the <code>android:fullBackupContent</code> attribute specifies an XML file - called {@code mybackupscheme.xml}, which resides in the <code>res/xml/</code> directory of your - app development project. This configuration file contains rules controlling which files are backed - up. The following example code shows a configuration file that excludes a specific file, - {@code device_info.db}: -</p> - -<pre> -<?xml version="1.0" encoding="utf-8"?> -<full-backup-content> - <exclude domain="database" path="device_info.db"/> -</full-backup-content> -</pre> - -<h3 id="auto-exclude">Automatically excluded data files</h3> - -<p> - Most apps do not need to, and in fact should not, back up all data. For example, the system - should not back up temporary files and caches. For this reason, the automatic backup - service excludes certain data files by default: -</p> - -<ul> - <li>Files in the directories to which the - {@link android.content.Context#getCacheDir getCacheDir()} and - {@link android.content.Context#getCodeCacheDir getCodeCacheDir()} methods refer. - </li> - - <li>Files located on external storage, unless they reside in the directory to which the - {@link android.content.Context#getExternalFilesDir getExternalFilesDir()} method refers. - </li> - - <li>Files located in the directory to which the - {@link android.content.Context#getNoBackupFilesDir getNoBackupFilesDir()} method refers. - </li> -</ul> -<h3>Backup Configuration Syntax</h3> - -<p> - The backup service configuration allows you to specify what files to include or exclude from - backup. The syntax for the data backup configuration XML file is as follows: -</p> - -<pre> -<full-backup-content> - <include domain=["file" | "database" | "sharedpref" | "external" | "root"] - path="string" /> - <exclude domain=["file" | "database" | "sharedpref" | "external" | "root"] - path="string" /> -</full-backup-content> -</pre> - -<p> - The following elements and attributes allow you to specify the files to include in, and exclude - from, backup: -</p> - -<ul> - <li> - <code><include></code>: Specifies a set of resources to - back up, instead of having the system back up all data in your app by default. If you specify - an <code><include></code> element, the system backs up <em>only the resources specified</em> - with this element. You can specify multiple sets of resources to back up by using multiple - <code><include></code> elements - </li> - - <li> - <code><exclude></code>: Specifies any data you want the system to exclude - when it does a full backup. If you target the same set of resources with both the - <code><include></code> and <code><exclude></code> elements, - <code><exclude></code> takes precedence. - </li> - - <li> - <code>domain</code>: Specifies the type of resource you want to include in, - or exclude from, backup. Valid values for this attribute include: - - - - <ul> - <li> - <code>root</code>: Specifies that the resource is in the app’s root directory. - </li> - - <li> - <code>file</code>: Specifies a resource in the directory returned by the - {@link android.content.Context#getFilesDir getFilesDir()} method. - </li> - - <li> - <code>database</code>: Specifies a database that the - {@link android.content.Context#getDatabasePath getDatabasePath()} method returns, or that - the app interacts with via the {@link android.database.sqlite.SQLiteOpenHelper} class. - </li> - - <li> - <code>sharedpref</code>: Specifies a {@link android.content.SharedPreferences} object - that the {@link android.content.Context#getSharedPreferences getSharedPreferences()} - method returns. - </li> - - <li> - <code>external</code>: Specifies that the resource is in external storage, and corresponds - to a file in the directory that the - {@link android.content.Context#getExternalFilesDir getExternalFilesDir()} method returns. - </li> - </ul> - </li> - <li> - <code>path</code>: Specifies the file path to a resource that you want to include in, or - exclude from, backup. - </li> - - </li> -</ul> - - -<h3 id="disabling">Disabling data backups</h3> - -<p> - You can choose to prevent automatic backups of any of your app data by setting the - <code>android:allowBackup</code> attribute to <code>false</code> in the {@code app} element of - your manifest. This setting is illustrated in the following example: -</p> - -<pre> -<?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - package="com.my.appexample"> - <uses-sdk android:minSdkVersion="23"/> - <uses-sdk android:targetSdkVersion="23"/> - <application ... -<strong> android:allowBackup="false"></strong> - </application> - ... -</manifest> -</pre> - -<h2 id="previous-androids">Support Lower Versions of Android</h2> - -<p>There are two scenarios in which you may also need to support versions of Android lower -than 6.0 (API level 23): You may be updating your existing app to take advantage of the -new auto backup functionality in Android 6.0, while wanting -to continue supporting earlier versions of Android. Or you may be releasing a new app, but -want to make sure devices running on versions of Android predating 6.0 also have backup -functionality.</p> - -<h3 id="updating">Updating an existing app to support auto backup</h3> - -<p>Earlier versions of Android supported a key/value-pair-based backup mechanism, in which the app -defines a subclass of {@link android.app.backup.BackupAgent} and sets -<a href="{@docRoot}guide/topics/manifest/application-element.html#agent"> -{@code android:backupAgent}</a> in its -<a href="{@docRoot}guide/topics/manifest/application-element.html">app manifest</a>. If your app -used this legacy approach, you can transition to full-data backups by adding the -{@code android:fullBackupOnly="true"} attribute to the -<a href="{@docRoot}guide/topics/manifest/application-element.html">{@code <application/>}</a> -element in the manifest. When running on a device with Android 5.1 -(API level 22) or lower, your app ignores this value in the manifest, and continues performing -backups in the previous manner.</p> - -<p>Even if you’re not using key/value backups, you can still use the approach described above to do -any custom processing in {@link android.app.Activity#onCreate(android.os.Bundle) onCreate()} -or {@link android.app.backup.BackupAgent#onFullBackup onFullBackup()}. You can also use that -approach to receive a notification when a restore operation happens in -{@link android.app.backup.BackupAgent#onRestoreFinished onRestoreFinished()}. If you want to retain -the system's default implementation of -<a href="#include-exclude">XML include/exclude rules handling</a>, call -{@link android.app.backup.BackupAgent#onFullBackup super.onFullBackup()}.</p> - -<h3 id="lower-versions">Giving your new app support for lower versions of Android</h3> - -<p>If you are creating a new app that targets Android 6.0, but you also want to enable cloud backup -for devices running on Android 5.1 (API level 22) and lower, you must also -<a href="{@docRoot}training/backup/backupapi.html">implement the Backup API</a>.</p> - -<h2 id="testing">Test Backup Configuration</h2> - -<p> - Once you have created a backup configuration, you should test it to make sure your app saves data - and can restore it properly. -</p> - - -<h3>Enabling Backup Logging</h3> - -<p> - To help determine how the backup feature is parsing your XML file, enable logging before - performing a test backup: -</p> - -<pre class="no-pretty-print"> -$ adb shell setprop log.tag.BackupXmlParserLogging VERBOSE -</pre> - -<h3>Testing Backup</h3> - -<p>To manually run a backup, first initialize the Backup Manager by executing the following - command: -</p> - -<pre class="no-pretty-print"> -$ adb shell bmgr run -</pre> - -<p> - Next, manually back up your application using the following command. Use the - <code><PACKAGE></code> parameter to specify the package name for your app: -</p> - -<pre class="no-pretty-print"> -$ adb shell bmgr fullbackup <PACKAGE></pre> - - -<h3>Testing restore</h3> - -<p> - To manually initiate a restore after the system has backed up your app data, execute the following - command, using the <code><PACKAGE></code> parameter to specify the package name for your - app: -</p> - -<pre class="noprettyprint"> -$ adb shell bmgr restore <PACKAGE> -</pre> - -<p class="warning"> - <b>Warning:</b> This action stops your app and wipes its data before performing the restore - operation. -</p> - -<p> - You can test automatic restore for your app by uninstalling and reinstalling your app. The app - data is automatically restored from the cloud once the app installation is complete. -</p> - - -<h3>Troubleshooting backups</h3> - -<p> - If backup fails, you can clear the backup data and associated metadata either by turning backup - off and on in <strong>Settings > Backup</strong>, factory-resetting the device, or - executing this command: -</p> - -<pre>$ adb shell bmgr wipe <TRANSPORT> <PACKAGE></pre> - -<p> - You must prepend <code>com.google.android.gms</code> to the {@code <TRANSPORT>} value. - To get the list of <a href="{@docRoot}google/backup/index.html">transports</a>, execute the - following command: -</p> - -<pre>$ adb shell bmgr list transports</pre> - -<h2 id="gcm">Handle Google Cloud Messaging</h2> - - <p> - For apps that use <a href="https://developers.google.com/cloud-messaging/gcm">Google Cloud - Messaging</a> (GCM) for push notifications, backing up the registration - token that Google Cloud Messaging registration returned can cause unexpected behavior in - notifications for the restored app. This is because when a user installs your app on a new device, - the app must <a href="https://developers.google.com/cloud-messaging/android/client#sample-register"> - query the GCM API for a new registration token</a>. If the old registration is present, because the - system had backed it up and restored it, the app doesn't seek the new token. To prevent this issue - from arising, exclude the registration token from the set of backed-up files. - </p> diff --git a/docs/html/training/backup/backupapi.jd b/docs/html/training/backup/backupapi.jd deleted file mode 100644 index 2f3e93943f37..000000000000 --- a/docs/html/training/backup/backupapi.jd +++ /dev/null @@ -1,200 +0,0 @@ -page.title=Using the Backup API -parent.title=Backing up App Data to the Cloud -parent.link=index.html - -trainingnavtop=true - -next.title=Making the Most of Google Cloud Messaging -next.link=gcm.html - -@jd:body - -<div id="tb-wrapper"> - <div id="tb"> - <h2>This lesson teaches you to</h2> - <ol> - <li><a href="#register">Register for the Android Backup Service</a></li> - <li><a href="#manifest">Configure Your Manifest</a></li> - <li><a href="#agent">Write Your Backup Agent</a></li> - <li><a href="#backup">Request a Backup</a></li> - <li><a href="#restore">Restore from a Backup</a></li> - </ol> - <h2>You should also read</h2> - <ul> - <li><a href="{@docRoot}guide/topics/data/backup.html">Data Backup</a></li> - <li><a href="{@docRoot}training/backup/autosyncapi.html">Configuring Auto Backup for Apps</a> - (Android 6.0 (API level 23) and higher)</li> - </ul> - </div> -</div> - -<p>When a user purchases a new device or resets their existing one, they might -expect that when Google Play restores your app back to their device during the -initial setup, the previous data associated with the app restores as well. On versions of Android -prior to 6.0 (API level 23), app data is not restored by default, and all the user's accomplishments -or settings in your app are lost.</p> -<p>For situations where the volume of data is relatively light (less than a -megabyte), like the user's preferences, notes, game high scores or other -stats, the Backup API provides a lightweight solution. This lesson walks you -through integrating the Backup API into your application, and restoring data to -new devices using the Backup API. - -<p class="note"> -<strong>Note:</strong> Devices running Android 6.0 and higher -<a href="{@docRoot}training/backup/autosyncapi.html">automatically back up</a> -nearly all data by default. -</p> - -<h2 id="register">Register for the Android Backup Service</h2> -<p>This lesson requires the use of the <a - href="{@docRoot}google/backup/index.html">Android Backup - Service</a>, which requires registration. Go ahead and <a - href="http://code.google.com/android/backup/signup.html">register here</a>. Once -that's done, the service pre-populates an XML tag for insertion in your Android -Manifest, which looks like this:</p> -<pre> -<meta-data android:name="com.google.android.backup.api_key" -android:value="ABcDe1FGHij2KlmN3oPQRs4TUvW5xYZ" /> -</pre> -<p>Note that each backup key works with a specific package name. If you have -different applications, register separate keys for each one.</p> - - -<h2 id="manifest">Configure Your Manifest</h2> -<p>Use of the Android Backup Service requires two additions to your application -manifest. First, declare the name of the class that acts as your backup agent, -then add the snippet above as a child element of the Application tag. Assuming -your backup agent is going to be called {@code TheBackupAgent}, here's an example of -what the manifest looks like with this tag included:</p> - -<pre> -<application android:label="MyApp" - android:backupAgent="TheBackupAgent"> - ... - <meta-data android:name="com.google.android.backup.api_key" - android:value="ABcDe1FGHij2KlmN3oPQRs4TUvW5xYZ" /> - ... -</application> -</pre> -<h2 id="agent">Write Your Backup Agent</h2> -<p>The easiest way to create your backup agent is by extending the wrapper class -{@link android.app.backup.BackupAgentHelper}. Creating this helper class is -actually a very simple process. Just create a class with the same name as you -used in the manifest in the previous step (in this example, {@code -TheBackupAgent}), -and extend {@code BackupAgentHelper}. Then override the {@link -android.app.backup.BackupAgent#onCreate()}.</p> - -<p>Inside the {@link android.app.backup.BackupAgent#onCreate()} method, create a {@link -android.app.backup.BackupHelper}. These helpers are -specialized classes for backing up certain kinds of data. The Android framework -currently includes two such helpers: {@link -android.app.backup.FileBackupHelper} and {@link -android.app.backup.SharedPreferencesBackupHelper}. After you create the helper -and point it at the data you want to back up, just add it to the -BackupAgentHelper using the {@link android.app.backup.BackupAgentHelper#addHelper(String, BackupHelper) addHelper()} -method, adding a key which is used to -retrieve the data later. In most cases the entire -implementation is perhaps 10 lines of code.</p> - -<p>Here's an example that backs up a high scores file.</p> - -<pre> -import android.app.backup.BackupAgentHelper; -import android.app.backup.FileBackupHelper; - - -public class TheBackupAgent extends BackupAgentHelper { - // The name of the SharedPreferences file - static final String HIGH_SCORES_FILENAME = "scores"; - - // A key to uniquely identify the set of backup data - static final String FILES_BACKUP_KEY = "myfiles"; - - // Allocate a helper and add it to the backup agent - @Override - void onCreate() { - FileBackupHelper helper = new FileBackupHelper(this, HIGH_SCORES_FILENAME); - addHelper(FILES_BACKUP_KEY, helper); - } -} -</pre> -<p>For added flexibility, {@link android.app.backup.FileBackupHelper}'s -constructor can take a variable number of filenames. You could just as easily -have backed up both a high scores file and a game progress file just by adding -an extra parameter, like this:</p> -<pre> -@Override - void onCreate() { - FileBackupHelper helper = new FileBackupHelper(this, HIGH_SCORES_FILENAME, PROGRESS_FILENAME); - addHelper(FILES_BACKUP_KEY, helper); - } -</pre> -<p>Backing up preferences is similarly easy. Create a {@link -android.app.backup.SharedPreferencesBackupHelper} the same way you did a {@link -android.app.backup.FileBackupHelper}. In this case, instead of adding filenames -to the constructor, add the names of the shared preference groups being used by -your application. Here's an example of how your backup agent helper might look if -high scores are implemented as preferences instead of a flat file:</p> - -<pre> -import android.app.backup.BackupAgentHelper; -import android.app.backup.SharedPreferencesBackupHelper; - -public class TheBackupAgent extends BackupAgentHelper { - // The names of the SharedPreferences groups that the application maintains. These - // are the same strings that are passed to getSharedPreferences(String, int). - static final String PREFS_DISPLAY = "displayprefs"; - static final String PREFS_SCORES = "highscores"; - - // An arbitrary string used within the BackupAgentHelper implementation to - // identify the SharedPreferencesBackupHelper's data. - static final String MY_PREFS_BACKUP_KEY = "myprefs"; - - // Simply allocate a helper and install it - void onCreate() { - SharedPreferencesBackupHelper helper = - new SharedPreferencesBackupHelper(this, PREFS_DISPLAY, PREFS_SCORES); - addHelper(MY_PREFS_BACKUP_KEY, helper); - } -} -</pre> - -<p>You can add as many backup helper instances to your backup agent helper as you -like, but remember that you only need one of each type. One {@link -android.app.backup.FileBackupHelper} handles all the files that you need to back up, and one -{@link android.app.backup.SharedPreferencesBackupHelper} handles all the shared -preferencegroups you need backed up. -</p> - - -<h2 id="backup">Request a Backup</h2> -<p>In order to request a backup, just create an instance of the {@link -android.app.backup.BackupManager}, and call it's {@link -android.app.backup.BackupManager#dataChanged()} method.</p> - -<pre> -import android.app.backup.BackupManager; -... - -public void requestBackup() { - BackupManager bm = new BackupManager(this); - bm.dataChanged(); -} -</pre> - -<p>This call notifies the backup manager that there is data ready to be backed -up to the cloud. At some point in the future, the backup manager then calls -your backup agent's {@link -android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor, BackupDataOutput, -ParcelFileDescriptor) onBackup()} method. You can make -the call whenever your data has changed, without having to worry about causing -excessive network activity. If you request a backup twice before a backup -occurs, the backup only occurs once.</p> - - -<h2 id="restore">Restore from a Backup</h2> -<p>Typically you shouldn't ever have to manually request a restore, as it -happens automatically when your application is installed on a device. However, -if it <em>is</em> necessary to trigger a manual restore, just call the -{@link android.app.backup.BackupManager#requestRestore(RestoreObserver) requestRestore()} method.</p> diff --git a/docs/html/training/backup/index.jd b/docs/html/training/backup/index.jd deleted file mode 100644 index 4449fde1d5c3..000000000000 --- a/docs/html/training/backup/index.jd +++ /dev/null @@ -1,47 +0,0 @@ -page.title=Backing up App Data to the Cloud -page.tags=cloud,sync,backup - -trainingnavtop=true -startpage=true - -@jd:body - -<div id="tb-wrapper"> -<div id="tb"> - -<h2>Dependencies and prerequisites</h2> -<ul> - <li>Android 2.2 (API level 8) and higher</li> -</ul> -</div> -</div> - -<p>Users often invest significant time and effort creating data and setting -preferences within apps. Preserving that data for users if they replace a broken -device or upgrade to a new one is an important part of ensuring a great user -experience.</p> - -<p>This class covers techniques for backing up data to the cloud so that -users can restore their data when recovering from a data loss (such as a factory -reset) or installing your application on a new device.</p> - -<p>It is important to note that the API for cloud backup changed with the -release of Android 6.0 (API level 23). For your app to support backup both -on devices running Android 6.0, and those running Android 5.1 (API level -22) and lower, you must implement both techniques that this class explains.</p> - -<h2>Lessons</h2> - -<dl> - <dt><strong><a href="autosyncapi.html">Configuring Auto Backup for Apps</a></strong></dt> - <dd>This lesson applies to Android 6.0 (API level 23) and higher. Learn how to accomplish - seamless app data backup and restore with zero additional lines of application code.</dd> -</dl> - -<dl> - <dt><strong><a href="backupapi.html">Using the Backup API</a></strong></dt> - <dd>This lesson applies to Android 5.1 (API level 22) and lower. Learn how to integrate the Backup - API into your Android app, so all of that app's user data, such as preferences, notes, and high - scores, updates seamlessly across all devices linked to that Google account.</dd> -</dl> - diff --git a/docs/html/training/best-performance.jd b/docs/html/training/best-performance.jd index 8ea6fd5be4e5..bb88e990ce48 100644 --- a/docs/html/training/best-performance.jd +++ b/docs/html/training/best-performance.jd @@ -5,4 +5,9 @@ page.trainingcourse=true <p>These classes and articles help you build an app that's smooth, responsive, -and uses as little battery as possible.</p>
\ No newline at end of file +and uses as little battery as possible.</p> + +<p>Along with this section, you can find additional information about optimizing +your app in the <a href="/topic/performance/index.html">Performance and +Power</a> section.</p> + diff --git a/docs/html/training/monitoring-device-state/battery-monitoring.jd b/docs/html/training/monitoring-device-state/battery-monitoring.jd index db75aaf1f899..bab9c9cf6baf 100644 --- a/docs/html/training/monitoring-device-state/battery-monitoring.jd +++ b/docs/html/training/monitoring-device-state/battery-monitoring.jd @@ -141,10 +141,11 @@ receiver. The receiver is triggered whenever the device battery becomes low or e condition by listening for {@link android.content.Intent#ACTION_BATTERY_LOW} and {@link android.content.Intent#ACTION_BATTERY_OKAY}.</p> -<pre><receiver android:name=".BatteryLevelReceiver"> -<intent-filter> - <action android:name="android.intent.action.ACTION_BATTERY_LOW"/> - <action android:name="android.intent.action.ACTION_BATTERY_OKAY"/> +<pre> +<receiver android:name=".BatteryLevelReceiver"> + <intent-filter> + <action android:name="android.intent.action.BATTERY_LOW"/> + <action android:name="android.intent.action.BATTERY_OKAY"/> </intent-filter> </receiver></pre> diff --git a/docs/html/training/training_toc.cs b/docs/html/training/training_toc.cs index d0dccba64d35..d2bf881859d5 100644 --- a/docs/html/training/training_toc.cs +++ b/docs/html/training/training_toc.cs @@ -649,25 +649,7 @@ </li> </ul> </li> - <li class="nav-section"> - <div class="nav-section-header"> - <a href="<?cs var:toroot ?>training/backup/index.html" - description= - "How to sync and back up app and user data to remote web services in the - cloud and how to restore the data back to multiple devices." - >Backing up App Data to the Cloud</a> - </div> - <ul> - <li><a href="<?cs var:toroot ?>training/backup/autosyncapi.html"> - Configuring Auto Backup - </a> - </li> - <li><a href="<?cs var:toroot ?>training/backup/backupapi.html"> - Using the Backup API - </a> - </li> - </ul> <li><a href="<?cs var:toroot ?>training/cloudsave/conflict-res.html" description= "How to design a robust conflict resolution strategy for apps that save data to the cloud." @@ -1888,6 +1870,12 @@ results." >Managing Your App's Memory</a> </li> <li> + <a href="<?cs var:toroot ?>training/articles/memory-overview.html" + description= + "How Android manages app process and memory allocation." + >Overview of Android Memory Management</a> + </li> + <li> <a href="<?cs var:toroot ?>training/articles/perf-tips.html" description= "How to optimize your app's performance in various ways to improve its diff --git a/docs/html/wear/images/partners/mkors.png b/docs/html/wear/images/partners/mkors.png Binary files differnew file mode 100644 index 000000000000..2f1e8ec3aad4 --- /dev/null +++ b/docs/html/wear/images/partners/mkors.png diff --git a/docs/html/wear/images/partners/nixon.png b/docs/html/wear/images/partners/nixon.png Binary files differnew file mode 100644 index 000000000000..8674da27974d --- /dev/null +++ b/docs/html/wear/images/partners/nixon.png diff --git a/docs/html/wear/images/partners/polar.png b/docs/html/wear/images/partners/polar.png Binary files differnew file mode 100644 index 000000000000..2faeb063e1ff --- /dev/null +++ b/docs/html/wear/images/partners/polar.png diff --git a/docs/html/wear/index.jd b/docs/html/wear/index.jd index f9bdef5ce6ce..1eb08bec612c 100644 --- a/docs/html/wear/index.jd +++ b/docs/html/wear/index.jd @@ -267,6 +267,9 @@ nonavpage=true <div class="col-4"> <img src="/wear/images/partners/mips.png" alt="MIPS"> </div> + <div class="col-4"> + <img src="/wear/images/partners/mkors.png" alt=Michael Kors"> + </div> <div class="col-4"> <img src="/wear/images/partners/mobvoi.png" alt="Mobvoi"> </div> @@ -277,6 +280,12 @@ nonavpage=true <img src="/wear/images/partners/nb.png" alt="New Balance"> </div> <div class="col-4"> + <img src="/wear/images/partners/nixon.png" alt="Nixon"> + </div> + <div class="col-4"> + <img src="/wear/images/partners/polar.png" alt="Polar"> + </div> + <div class="col-4"> <img src="/wear/images/partners/qualcomm.png" alt="Qualcomm"> </div> <div class="col-4"> diff --git a/docs/html/wear/preview/_book.yaml b/docs/html/wear/preview/_book.yaml index a231fb5daa24..54d5442ec631 100644 --- a/docs/html/wear/preview/_book.yaml +++ b/docs/html/wear/preview/_book.yaml @@ -8,18 +8,24 @@ toc: - title: API Overview path: /wear/preview/api-overview.html section: - - title: Notification Improvements - path: /wear/preview/features/notifications.html - - title: Input Method Framework - path: /wear/preview/features/ime.html - title: Complications path: /wear/preview/features/complications.html - title: Navigation and Actions path: /wear/preview/features/ui-nav-actions.html + - title: Curved Layout + path: /wear/preview/features/wearable-recycler-view.html + - title: Notification Improvements + path: /wear/preview/features/notifications.html - title: Bridging for Notifications path: /wear/preview/features/bridger.html + - title: Input Method Framework + path: /wear/preview/features/ime.html - title: Wrist Gestures path: /wear/preview/features/gestures.html + - title: Standalone apps + path: /wear/preview/features/standalone-apps.html + - title: App Distribution + path: /wear/preview/features/app-distribution.html - title: Get Started path: /wear/preview/start.html diff --git a/docs/html/wear/preview/api-overview.jd b/docs/html/wear/preview/api-overview.jd index 0b3ac6bf4b2d..f4612a259958 100644 --- a/docs/html/wear/preview/api-overview.jd +++ b/docs/html/wear/preview/api-overview.jd @@ -13,15 +13,16 @@ page.image=images/cards/card-n-apis_2x.png <ol> <li><a href="#complications">Complications</a></li> <li><a href="#drawers">Navigation and Action Drawers</a></li> + <li><a href="#wrv">Curved Layout</a></li> </ol> </li> <li><a href="#notify">Notifications and Input</a> <ol> - <li><a href="#expanded">Expanded Notification</a></li> - <li><a href="#messaging">Messaging Style Notification</a></li> + <li><a href="#expanded">Expanded Notifications</a></li> + <li><a href="#messaging">Messaging Style Notifications</a></li> + <li><a href="#inline-action">Inline Action</a></li> <li><a href="#smart-replies">Smart Reply</a></li> - <li><a href="#content-action">Notification Content Action</a> <li><a href="#remote-input">Remote Input</a></li> <li><a href="#bridging">Bridging Mode</a></li> <li><a href="#imf">Input Method Framework</a></li> @@ -40,308 +41,447 @@ page.image=images/cards/card-n-apis_2x.png </div> </div> - - -<p> - The Android Wear Preview API is still in active development, but you can try - it now as part of the Wear 2.0 Developer Preview. The sections below - highlight some of the new features for Android Wear developers. -</p> - - -<h2 id="ui">User Interface Improvements</h2> - -<p> - The preview introduces powerful additions to the user interface, opening up - exciting possibilities to developers. A complication - is any feature in a watch face that displays more than hours and - minutes. With the Complications API, watch faces can display extra information - and separate apps can expose complication data. The navigation and action - drawers provide users with new ways to interact with apps. -</p> - - -<h3 id="complications">Complications</h3> -<img src="{@docRoot}wear/preview/images/complications-main-image.png" - height="320" style="float:right;margin:10px 0 0 40px" /> - -<p> - A <a href= - "https://en.wikipedia.org/wiki/Complication_(horology)">complication</a> is a - feature of a watch face that displays more than hours and minutes, such as a - battery indicator or a step counter. The Complications API thus helps watch face - developers create visual features and the data connections they - require. -</p> - -<p> - Watch faces that use this API can display extra information without needing - code for getting the underlying data. Data providers can supply data to any - watch face using the API. -</p> - -<p>For information about this API, -see <a href="{@docRoot}wear/preview/features/complications.html"> - Watch Face Complications</a>. -</p> - -<h3 id="drawers">Navigation and Action drawers</h3> - -<p>Wear 2.0 introduces two new widgets, navigation drawer and action drawer. These - widgets give your users new ways to interact with your app. The navigation drawer - appears at the top of the screen and allows users to navigate between app views. - The action drawer appears at the bottom of the screen and allows users to choose - from a list of actions associated with the current usage context. These drawers - are accessible to users when they edge swipe from the top or bottom of the - screen; they peek when users scroll in an opposite direction. -</p> - -<div class="cols"> - <div class="col-2of6"> - <img src="{@docRoot}wear/preview/images/nav_drawer.gif" - height="240" alt="" style="padding:.5em"> - </div> - <div class="col-2of6"> - <img src="{@docRoot}wear/preview/images/action_drawer.gif" - height="240" alt="" style="padding:.5em;"> - </div> -</div> - -<p> - To learn how to add these widgets to your app, see - <a href="{@docRoot}wear/preview/features/ui-nav-actions.html"> - Wear Navigation and Actions</a>. -</p> - - -<h2 id="notify">Notifications and Input</h2> - -<p>In Wear 2.0, we’ve redesigned the key experiences on the watch to be even more - intuitive and provide users new ways to respond to messages. Some of the highlights - are below; for a complete list of changes, see - <a href="{@docRoot}wear/preview/features/notifications.html">Notification Changes in Wear 2.0</a>. - - -<img src="{@docRoot}wear/preview/images/expanded_diagram.png" height="340" - style="float:left;margin:10px 20px 0 0" /> -<h3 id="expanded">Expanded notifications</h3> - -<p> - When a user taps on a notification that is bridged from the phone to the - watch or that lacks a - <a href="{@docRoot}reference/android/support/v4/app/NotificationCompat.Builder.html#setContentIntent(android.app.PendingIntent)"> - {@code contentIntent}</a>, the user will be taken to the expanded view of - that notification. When you <a href= - "{@docRoot}training/wearables/notifications/pages.html">specify additional - content pages</a> and actions for a notification, those are available to the - user within the expanded notification. Each expanded notification follows - <a href="https://google.com/design/spec-wear">Material Design for Android - Wear</a>, so the user gets an app-like experience. -</p> - - -<h3 id="messaging">Messaging Style notification</h3> -<p> If you have a chat messaging app, your notifications should use -{@code Notification.MessagingStyle}, which is new in Android 6.0. Wear 2.0 uses -the chat messages included in a -<a href="{@docRoot}preview/features/notification-updates.html#style">{@code MessagingStyle}</a> - notification -(see {@code addMessage()}) to provide a rich chat app-like experience in the -expanded notification. -</p> - - -<h3 id="smart-replies">Smart Reply</h3> - -<p>Android Wear 2.0 introduces support for Smart Reply in -<a href="{@docRoot}wear/preview/features/notifications.html#messaging">{@code MessagingStyle}</a> - notifications. Smart Reply provides the user with contextually relevant, - touchable choices in the expanded notification and in - <a href="{@docRoot}reference/android/app/RemoteInput.html">{@code RemoteInput}</a>. -</p> - -<p>By enabling Smart Reply for your {@code MessagingStyle} notifications, you provide -users a fast (single tap), discreet (no speaking aloud), and reliable way to respond - to chat messages they receive. - </p> - - -<img src="{@docRoot}wear/preview/images/remoteinput.png" height="350" - style="float:right;margin:10px 0 0 40px" /> - -<h3 id="remote-input">Remote Input</h3> - -<p>Wear 2.0 users can choose between various input options from -<a href="{@docRoot}reference/android/app/RemoteInput.html">Remote Input</a>. - These options include: -</p> -<ul> -<li>Dictation</li> -<li>Emoji</li> -<li>Canned responses</li> -<li>Smart Reply</i> -<li>Default IME </i> -</ul> - -<p> -For messaging notifications with Smart Reply, the system-generated Smart Reply - appears within <a href="{@docRoot}reference/android/app/RemoteInput.html">{@code RemoteInput}</a> - above the developer-provided list of canned responses. - You can also use the - <a href="{@docRoot}reference/android/app/RemoteInput.Builder.html#setChoices(java.lang.CharSequence[])">setChoices()</a> - method in the {@code RemoteInput} API to enable users to select from a list - of canned responses. -</p> - -<h3 id="bridging"> Bridging Mode </h3> -<p>By default, notifications are -<a href="{@docRoot}training/wearables/notifications/index.html"> -bridged</a> (shared) from an app on a companion phone -to the watch. Since a phone app and a standalone watch app may be sources of the - same notifications, the Android Wear 2.0 Preview includes a Bridging mode feature. - Developers can begin planning to change the behavior of notifications with the - following: -</p> - -<ul> -<li>Specifying in the standalone app's Android manifest file that notifications from - the corresponding phone app should not be bridged to the watch. </li> -<li>Setting a dismissal ID so notification dismissals (by users) are synced across -devices.</li> -</ul> - -<p>For an example of how to use this feature, see <a href="{@docRoot}wear/preview/features/bridger.html"> -Bridging Mode for Notifications</a>.</p> - -<h3 id="imf">Input Method Framework</h3> - -<p>Wear 2.0 extends the Android input method framework (IMF) to Android Wear. -This allows users to enter text on Wear using the system default IME or third party - IMEs. The Wear IME lets the user enter text via gesture typing as well as tapping - individual keys. The IMF APIs used for Wear devices are the same as other form - factors, though usage is slightly different due to limited screen real estate. -</p> - -<p>Wear provides user settings on the watch that let the user:</p> -<ul> -<li>Enable multiple IMEs from the list of installed IMEs.</li> -<li>Set a single default IME from the list of enabled IMEs.</li> -<li>Change languages for various IMEs.</li> -</ul> - -<p>To learn how to create an IME for Wear, see <a href="{@docRoot}wear/preview/features/ime.html"> -Input Method Framework</a>. -</p> - -<h3 id="wrist-gestures">Wrist Gestures</h3> - -<p> - Wrist gestures can enable quick, one-handed interactions with your app - when use of a touch screen is inconvenient. The following - <a href="https://support.google.com/androidwear/answer/6312406">wrist gestures</a> - are available for use by apps: -</p> - -<ul> - <li>Flick wrist out</li> - <li>Flick wrist in</li> -</ul> - -<p>For more information, see -<a href="{@docRoot}wear/preview/features/gestures.html"> - Wrist Gestures</a>. -</p> - -<h2 id="stand-alone">Standalone Devices</h2> - -<p>Standalone watches will enable Android Wear apps to work independently of phone - apps. This means your app can continue to offer full functionality even if the - paired phone is far away or turned off. </p> - -<h3 id="wear-apk">Wear-Specific APKs</h3> - -<p>For delivery to a watch, an Android Wear app is currently embedded in its corresponding -phone app. This delivery method can result in an increased download size for users, - regardless of whether they have an Android Wear device. -</p> - -<p>With standalone devices, the -<a href ="{@docRoot}google/play/publishing/multiple-apks.html">Multi-APK</a> - delivery method will be used. Developers will have the ability to release Android - Wear apps independently of the corresponding phone apps. Please stay tuned for - more information about this change. -</p> - -<h3 id="network">Network Access</h3> - -<p>Since Android Wear apps will work independently of phone apps, Android Wear's - network access will no longer require the - <a href="{@docRoot}training/wearables/data-layer/index.html"> - Wearable Data Layer API</a>. Android Wear apps will have the ability to make - their own network requests. Additionally, they will be able to directly use - Google Cloud Messaging. -</p> - -<p>No APIs for network access or GCM are specific to Android Wear; refer to the -existing documentation about -<a href="{@docRoot}training/basics/network-ops/connecting.html"> -Connecting to the Network</a> and -<a href="https://developers.google.com/cloud-messaging/">Cloud Messaging</a>. -</p> - -<p>We recommend using the following libraries:</p> -<ul> -<li><a href="{@docRoot}reference/android/app/job/JobScheduler.html"> -JobScheduler</a> for asynchronous jobs, including polling at regular intervals -</li> -<li>Multi-networking APIs if you need to connect to specific network types; see -the <a href="{@docRoot}about/versions/android-5.0.html#Wireless"> -Multiple Network Connections</a> -</li> -</ul> - -<p>You will still be able to use the -<a href="{@docRoot}training/wearables/data-layer/index.html"> - Wearable Data Layer API</a> to communicate with a phone app. - However, use of this API to connect to a network will be discouraged. - </p> - - -<h3 id="auth">Authentication</h3> - -<p>Since Android Wear apps will work independently of phone apps, Android Wear's - authentication capabilities will be more powerful; apps will have new ways to - authenticate.</p> - -<h4>Users can enter a username and password on a watch</h4> - -<p>Google Keyboard will be standard on Android Wear, allowing for direct text -entry. This feature will work as expected with standard -<a href="{@docRoot}reference/android/widget/EditText.html">EditText widgets</a>. -For passwords, the {@code textPassword} attribute will be used.</p> - -<h4>Utilizing Account Manager</h4> - -<p>Android Wear will include the -<a href="{@docRoot}reference/android/accounts/AccountManager.html"> -AccountManager</a>, which will be accessible for syncing and storing account -data, as it is on an Android phone.</p> - -<h4>Authentication tokens can be passed over the Wearable Data Layer</h4> - -<p>For Android-paired watches (only), a phone securely -transfers authentication credentials to a watch app via the -<a href="{@docRoot}training/wearables/data-layer/index.html"> -Wearable Data Layer API</a>. The credentials can be transferred as -messages or data items.</p> - -<p>If your watch app needs to determine if your phone app is installed, you can -advertise a capability on the phone app and retrieve the capability on the -watch. For more information, see the following sections of -<a href="{@docRoot}training/wearables/data-layer/messages.html"> -Sending and Receiving Messages</a>:</p> - -<ul> - <li>Advertise Capabilities</li> - <li>Retrieve the Nodes with the Required Capabilities</li> -</ul> + <p> + Wear 2.0 is still in active development, but you can try it as part of + the Wear 2.0 Developer Preview. The sections below highlight some of the + new features for developers. + </p> + + <h2 id="ui"> + User Interface Improvements + </h2> + + <p> + The preview introduces powerful additions to the user interface, opening + up exciting possibilities to developers. + </p> + + <h3 id="complications"> + Complications + </h3> + + <img src="{@docRoot}wear/preview/images/complications-main-image.png" + height="320" style="float:right;margin:10px 0 0 40px"> + <p> + A <a href= + "https://en.wikipedia.org/wiki/Complication_(horology)">complication</a> + is a feature of a watch face that displays more than hours and minutes, + such as a battery indicator or a step counter. The Complications API thus + helps watch face developers create visual features and the data + connections they require. + </p> + + <p> + Watch faces that use this API can display extra information without + needing code for getting the underlying data. Data providers can supply + data to any watch face using the API. + </p> + + <p> + For information about this API, see <a href= + "{@docRoot}wear/preview/features/complications.html">Watch Face + Complications</a>. + </p> + + <h3 id="drawers"> + Navigation and Action Drawers + </h3> + + <p> + Wear 2.0 introduces two new widgets, navigation drawer and action drawer. + These widgets give your users new ways to interact with your app. The + navigation drawer appears at the top of the screen and allows users to + navigate between app views. The action drawer appears at the bottom of + the screen and allows users to choose from a list of actions associated + with the current usage context. These drawers are accessible to users + when they edge swipe from the top or bottom of the screen; they peek when + users scroll in an opposite direction. + </p> + + <div class="cols"> + <div class="col-2of6"> + <img src="{@docRoot}wear/preview/images/nav_drawer.gif" height="240" + alt="" style="padding:.5em"> + </div> + + <div class="col-2of6"> + <img src="{@docRoot}wear/preview/images/action_drawer.gif" height="240" + alt="" style="padding:.5em;"> + </div> + </div> + + <p> + To learn how to add these widgets to your app, see <a href= + "{@docRoot}wear/preview/features/ui-nav-actions.html">Wear Navigation and + Actions</a>. + </p> + + <h3 id="wrv"> + Curved Layout + </h3> + + <p> + Wear 2.0 introduces the <code>WearableRecyclerView</code> class for + displaying and manipulating a vertical list of items, + optimized for round displays. + </p> + + <p> + The key features include the following: + </p> + + <ul> + <li>A curved layout on round devices + </li> + + <li>A circular scrolling gesture, which can be toggled on and off + </li> + </ul> + + <p> + To learn how to create a curved layout optimized for round devices, see + <a href= + "https://developer.android.com/wear/preview/features/wearable-recycler-view.html"> + Curved Layout</a>. + </p> + + <h2 id="notify"> + Notifications and Input + </h2> + + <p> + In Wear 2.0, we’ve redesigned the key experiences on the watch to be even + more intuitive and provide users new ways to respond to messages. Some of + the highlights are below; for a complete list of changes, see <a href= + "{@docRoot}wear/preview/features/notifications.html">Notification Changes + in Wear 2.0</a>. <img src= + "{@docRoot}wear/preview/images/expanded_diagram.png" height="340" style= + "float:left;margin:10px 20px 0 0"> + </p> + + <h3 id="expanded"> + Expanded Notifications + </h3> + + <p> + When a user taps on a notification that is bridged from the phone to the + watch or that lacks a <a href= + "{@docRoot}reference/android/support/v4/app/NotificationCompat.Builder.html#setContentIntent(android.app.PendingIntent)"> + {@code contentIntent}</a>, the user will be taken to the expanded view of + that notification. When you <a href= + "{@docRoot}training/wearables/notifications/pages.html">specify + additional content pages</a> and actions for a notification, those are + available to the user within the expanded notification. Each expanded + notification follows <a href= + "https://google.com/design/spec-wear">Material Design for Android + Wear</a>, so the user gets an app-like experience. + </p> + + <h3 id="messaging"> + Messaging Style Notifications + </h3> + + <p> + If you have a chat messaging app, your notifications should use {@code + Notification.MessagingStyle}, which is new in Android 6.0. Wear 2.0 uses + the chat messages included in a <a href= + "{@docRoot}preview/features/notification-updates.html#style">{@code + MessagingStyle}</a> notification (see {@code addMessage()}) to provide a + rich chat, app-like experience in the expanded notification. + </p> + + <h3 id="inline-action"> + Inline Action + </h3> + + <p> + Wear 2.0 enables you to add an inline action within the notification + stream so that users can quickly take an action on a notification. + Examples of good use cases for an inline action within a notification stream + include replying to a text message, stopping a fitness activity, or + archiving an email message. + </p> + + <p> + To learn how to add an inline action to your notification stream, see + <a href= + "https://developer.android.com/wear/preview/features/notifications.html#inline"> + Inline Action</a>. + </p> + + <h3 id="smart-replies"> + Smart Reply + </h3> + + <p> + Android Wear 2.0 introduces support for Smart Reply in <a href= + "{@docRoot}wear/preview/features/notifications.html#messaging">{@code + MessagingStyle}</a> notifications. Smart Reply provides the user with + contextually relevant, touchable choices in the expanded notification and + in <a href="{@docRoot}reference/android/app/RemoteInput.html">{@code + RemoteInput}</a>. + </p> + + <p> + By enabling Smart Reply for your {@code MessagingStyle} notifications, + you provide users a fast (single tap), discreet (no speaking aloud), and + reliable way to respond to chat messages they receive. + </p> + + <img src="{@docRoot}wear/preview/images/remoteinput.png" height="350" + style="float:right;margin:10px 0 0 40px"> + + <h3 id="remote-input"> + Remote Input + </h3> + + <p> + Wear 2.0 users can choose between various input options from <a href= + "{@docRoot}reference/android/app/RemoteInput.html">Remote Input</a>. + These options include: + </p> + + <ul> + <li>Dictation + </li> + + <li>Emoji + </li> + + <li>Canned responses + </li> + + <li>Smart Reply + </li> + + <li>Default IME + </li> + </ul> + + <p> + For messaging notifications with Smart Reply, the system-generated Smart + Reply appears within <a href= + "{@docRoot}reference/android/app/RemoteInput.html">{@code + RemoteInput}</a> above the developer-provided list of canned responses. + You can also use the <a href= + "{@docRoot}reference/android/app/RemoteInput.Builder.html#setChoices(java.lang.CharSequence[])"> + setChoices()</a> method in the {@code RemoteInput} API to enable users to + select from a list of canned responses. + </p> + + <h3 id="bridging"> + Bridging Mode + </h3> + + <p> + By default, notifications are <a href= + "{@docRoot}training/wearables/notifications/index.html">bridged</a> + (shared) from an app on a companion phone to the watch. Since a phone app + and a standalone watch app may be sources of the same notifications, the + Android Wear 2.0 Preview includes a Bridging mode feature. + </p> + + <p> + For information about this feature, see <a href= + "{@docRoot}wear/preview/features/bridger.html">Bridging Mode for + Notifications</a>. + </p> + + <h3 id="imf"> + Input Method Framework + </h3> + + <p> + Wear 2.0 extends the Android input method framework (IMF) to Android + Wear. This allows users to enter text on Wear using the system default + IME or third party IMEs. The Wear IME lets the user enter text via + gesture typing as well as tapping individual keys. The IMF APIs used for + Wear devices are the same as other form factors, though usage is slightly + different due to limited screen real estate. + </p> + + <p> + Wear provides user settings on the watch that let the user: + </p> + + <ul> + <li>Enable multiple IMEs from the list of installed IMEs. + </li> + + <li>Set a single default IME from the list of enabled IMEs. + </li> + + <li>Change languages for various IMEs. + </li> + </ul> + + <p> + To learn how to create an IME for Wear, see <a href= + "{@docRoot}wear/preview/features/ime.html">Input Method Framework</a>. + </p> + + <h3 id="wrist-gestures"> + Wrist Gestures + </h3> + + <p> + Wrist gestures can enable quick, one-handed interactions with your app + when use of a touch screen is inconvenient. The following <a href= + "https://support.google.com/androidwear/answer/6312406">wrist + gestures</a> are available for use by apps: + </p> + + <ul> + <li>Flick wrist out + </li> + + <li>Flick wrist in + </li> + </ul> + + <p> + For more information, see <a href= + "{@docRoot}wear/preview/features/gestures.html">Wrist Gestures</a>. + </p> + + <h2 id="stand-alone"> + Standalone Devices + </h2> + + <p> + Standalone watches enable Android Wear apps to work independently of + phone apps. This means your app can continue to offer full functionality + even if the paired phone is far away or turned off. + </p> + + <h3 id="wear-apk"> + Wear-Specific APKs + </h3> + + <p> + For delivery to a watch, an Android Wear app is currently embedded in its + corresponding phone app. This delivery method can result in an increased + download size for users, regardless of whether they have an Android Wear + device. + </p> + + <p> + For information about planning and building your standalone app + for Wear 2.0, see <a href= + "https://developer.android.com/wear/preview/features/standalone-apps.html"> + Standalone Apps</a>. + </p> + + <p> + For information about distributing your app, see <a href= + "https://developer.android.com/wear/preview/features/app-distribution.html"> + App Distribution</a>. + </p> + + <h3 id="network"> + Network Access + </h3> + + <p> + Since Android Wear apps will work independently of phone apps, Android + Wear's network access will no longer require the <a href= + "{@docRoot}training/wearables/data-layer/index.html">Wearable Data Layer + API</a>. Android Wear apps will have the ability to make their own + network requests. Additionally, they will be able to directly use Google + Cloud Messaging. For more information, see + <a href= + "https://developer.android.com/wear/preview/features/standalone-apps.html#network_access"> + Network Access and Cloud Messaging</a>. + </p> + + <p> + No APIs for network access or GCM are specific to Android Wear; refer to + the existing documentation about <a href= + "{@docRoot}training/basics/network-ops/connecting.html">Connecting to the + Network</a> and <a href= + "https://developers.google.com/cloud-messaging/">Cloud Messaging</a>. + </p> + + <p> + We recommend using the following libraries: + </p> + + <ul> + <li> + <a href= + "{@docRoot}reference/android/app/job/JobScheduler.html">JobScheduler</a> + for asynchronous jobs, including polling at regular intervals + </li> + + <li>Multi-networking APIs, to connect to specific network + types; see <a href= + "{@docRoot}about/versions/android-5.0.html#Wireless">Multiple Network + Connections</a> + </li> + </ul> + + <p> + The <a href= + "{@docRoot}training/wearables/data-layer/index.html">Wearable Data Layer + API</a> is available to communicate with a phone app. + However, use of this API to connect to a network will be discouraged. + </p> + + <h3 id="auth"> + Authentication + </h3> + + <p> + Since Android Wear apps will work independently of phone apps, Android + Wear's authentication capabilities will be more powerful; apps will have + new ways to authenticate. + </p> + + <h4> + Users can enter a username and password on a watch + </h4> + + <p> + Google Keyboard will be standard on Android Wear, allowing for direct + text entry. This feature will work as expected with standard <a href= + "{@docRoot}reference/android/widget/EditText.html">EditText widgets</a>. + For passwords, the {@code textPassword} attribute will be used. + </p> + + <h4> + Utilizing Account Manager + </h4> + + <p> + Android Wear will include the <a href= + "{@docRoot}reference/android/accounts/AccountManager.html">AccountManager</a>, + which will be accessible for syncing and storing account data, as it is + on an Android phone. + </p> + + <h4> + Authentication tokens can be passed over the Wearable Data Layer + </h4> + + <p> + For Android-paired watches (only), a phone securely transfers + authentication credentials to a watch app via the <a href= + "{@docRoot}training/wearables/data-layer/index.html">Wearable Data Layer + API</a>. The credentials can be transferred as messages or data items. + </p> + + <p> + If your watch app needs to determine if your phone app is installed, you + can advertise a capability on the phone app and retrieve the capability + on the watch. For more information, see the following sections of + <a href="{@docRoot}training/wearables/data-layer/messages.html">Sending + and Receiving Messages</a>: + </p> + + <ul> + <li>Advertise Capabilities + </li> + + <li>Retrieve the Nodes with the Required Capabilities + </li> + </ul> diff --git a/docs/html/wear/preview/behavior-changes.jd b/docs/html/wear/preview/behavior-changes.jd index 02146220dbdd..c93d337a0a8c 100644 --- a/docs/html/wear/preview/behavior-changes.jd +++ b/docs/html/wear/preview/behavior-changes.jd @@ -22,6 +22,8 @@ page.tags="preview", "developer preview" <ul> <li><a href="#activity-dismissal">Activity Dismissal</a></li> + <li><a href="#invalid-fields">Invalid Fields for a Complication Type</a></li> + <li><a href="#empty">Complication Types for Empty Data</a></li> </ul> </div> @@ -61,3 +63,44 @@ page.tags="preview", "developer preview" <a href="{@docRoot}wear/preview/features/ui-nav-actions.html">navigation drawers</a>. </p> + +<h2 id="invalid-fields">Invalid Fields for a Complication Type</h2> + +<p> + When a watch face uses the <a href="{@docRoot}wear/preview/features/complications.html"> + Complications API</a>, the watch face requests data from a chosen provider. + A <code>ComplicationData</code> object, which contains + complication types, is returned. +</p> + +<p> + A complication type determines the + kinds of data that a watch face can render. This section describes + a behavior change related to the <code>ComplicationData</code> object. +</p> + +<p> + Starting with + <a href="https://developer.android.com/wear/preview/support.html#dp3"> + Developer Preview 3</a>, when a watch face requests a field that is invalid + for a complication type, a default value for the field is returned. + For example, if a watch face tries to access a <code>Long text</code> + field in a <code>SHORT_TEXT</code> type, the default value for the + <code>Long text</code> field is returned. + In previous releases, such a request for an invalid field + (for a type) resulted in an exception. +</p> + +<h2 id="empty">Complication Types for Empty Data</h2> + +<p> + Starting with + <a href="https://developer.android.com/wear/preview/support.html#dp3"> + Developer Preview 3</a>, the complication types used for "empty" data are + changed. Apps that use the Complications API + may need to be updated to use + <code>TYPE_NO_DATA</code>. See the information + about <code>TYPE_NO_DATA</code> in the + <a href="{@docRoot}wear/preview/features/complications.html#types_and_fields"> + Types and fields</a> section. +</p> diff --git a/docs/html/wear/preview/downloads.jd b/docs/html/wear/preview/downloads.jd index 4bc401bae072..83a3f98452c8 100644 --- a/docs/html/wear/preview/downloads.jd +++ b/docs/html/wear/preview/downloads.jd @@ -171,7 +171,9 @@ This is the Android Wear SDK Preview License Agreement (the “License Agreement <li> <a href="#set_up_a_watch">Set Up a Watch</a> </li> - + <li> + <a href="#set_up_a_phone">Set Up a Phone</a> + </li> <li> <a href="#set_up_an_emulator">Set Up an Emulator</a> </li> @@ -180,7 +182,7 @@ This is the Android Wear SDK Preview License Agreement (the “License Agreement </div> <p> - You can run and test your app with the Android Wear 2.0 Developer Preview + You can run and test your app with the Android Wear 2.0 Preview in either of these ways: </p> @@ -237,6 +239,13 @@ This is the Android Wear SDK Preview License Agreement (the “License Agreement following tables and flash it to the corresponding device. </p> + <p class="caution"><strong>Caution:</strong> + After you flash an image to a watch, follow the steps for + <a href="#set_up_a_phone">setting up a phone</a> with the beta version of + the Android Wear companion app. To use a Wear 2.0 image on a watch, + you must have the beta companion app on a paired phone. + </p> + <p> To restore your device to its original state during the preview, you can flash the appropriate retail system image, below, to the device. @@ -266,9 +275,9 @@ This is the Android Wear SDK Preview License Agreement (the “License Agreement <td> Preview image for testing </td> - <td><a href="#top" onclick="onDownload(this)">nemo-nvd83h-factory-48ac950c.tgz</a><br> - MD5: dd351884cce9fb5bf1bdec0a8e5f56e3<br> - SHA-1: 48ac950c48faef96a7770e3c1acb56d23a28d859 + <td><a href="#top" onclick="onDownload(this)">nemo-nve68j-factory-302a33ea.tgz</a><br> + MD5: ddfccc3e050c7e2db8d657c82f7d6291<br> + SHA-1: 302a33eac348c401fcb165bad4b9aaa40c7beb2b </td> </tr> @@ -276,9 +285,9 @@ This is the Android Wear SDK Preview License Agreement (the “License Agreement <td> Non-preview image (for after testing) </td> - <td><a href="#top" onclick="onDownload(this)">nemo-mnc40x-factory-fa528bec.tgz</a><br> - MD5: 0b8ba3653d5a93cb854f4d7409d7b6c9<br> - SHA-1: fa528bec8aba3bf6c7d901ba63cd6ea0a08dbeb0 + <td><a href="#top" onclick="onDownload(this)">nemo-mfd18l-factory-3faf6f2d.tgz</a><br> + MD5: f3a0090c0e99da82ad095b5d2a9acc6d<br> + SHA-1: 3faf6f2d7f422a17a5f6c54cf5e1d2c5622689b0 </td> </tr> @@ -307,18 +316,18 @@ This is the Android Wear SDK Preview License Agreement (the “License Agreement <td> Preview image for testing </td> - <td><a href="#top" onclick="onDownload(this)">sturgeon-nvd83h-factory-cb5a11ab.tgz</a><br> - MD5: 38c1047992b1d28f6833d9f6c8470cdc<br> - SHA-1: cb5a11ab0260ea3ca7da5894e73e41f70357da6b + <td><a href="#top" onclick="onDownload(this)">sturgeon-nve68j-factory-6607cd31.tgz</a><br> + MD5: f78ac6ba8bb84038d163cc2d7ca85040<br> + SHA-1: 6607cd31858af1bfd50b905c68f7cf1f0b6e570e </td> </tr> <tr id="sturgeon-non-preview"> <td> Non-preview image (for after testing) </td> - <td><a href="#top" onclick="onDownload(this)">sturgeon-mec23l-factory-48003078.tgz</a><br> - MD5: 417b5cbddb29a2262bce133e283d2732<br> - SHA-1: 4800307843580f818557dd7c43d8ba2161e289b2 + <td><a href="#top" onclick="onDownload(this)">sturgeon-m6e69f-factory-e659286a.tgz</a><br> + MD5: 12ce6cb0b0e43b67ea46a886eae052ae<br> + SHA-1: e659286aa9004f4555a476ede4e8b690f56cfefd </td> </tr> </table> @@ -437,13 +446,14 @@ This is the Android Wear SDK Preview License Agreement (the “License Agreement <h4 id="set_up_watch"> - Set up the watch and begin testing + Set up the watch </h4> <p> After the <code>flash-all</code> script finishes, your watch reboots. - Pair the watch with a phone or tablet. The preview now is available - for testing on the watch. Before installing an app, perform the + Only pair the watch with a phone (so you can begin testing the preview) + by using the instructions in <a href="#set_up_a_phone">Set Up a Phone</a>. + Additionally, before installing an app, perform the following steps on the watch to re-secure the watch's bootloader: </p> @@ -483,7 +493,9 @@ This is the Android Wear SDK Preview License Agreement (the “License Agreement </ol> <p> - Your watch is ready for you to <a href= + After you follow the instructions in + <a href="#set_up_a_phone">Set Up a Phone</a>, + your watch will be ready for you to <a href= "{@docRoot}training/wearables/apps/creating.html#Install">install and run your app</a>: </p> @@ -539,13 +551,109 @@ This is the Android Wear SDK Preview License Agreement (the “License Agreement device reset and removes all user data on the device. </p> + <h2 id="set_up_a_phone"> + Set Up a Phone + </h2> + + <p> + On a phone, follow the instructions in this section to install the beta + version of the Android Wear companion app. The beta version cannot be run + on a phone at the same time as the non-beta version. Additionally, the + beta version is English-only. + </p> + + <p> + <p class="caution"><strong>Caution:</strong> If you have an existing + pairing of the phone to a Wear 1.x + watch, installation of the beta companion app will cause a loss of that + pairing. + </p> + + <h3 id="join-the-wear-2-0-preview-group"> + Join the Wear 2.0 preview group + </h3> + + <p> + To access the beta companion app, you must <a href= + "https://groups.google.com/forum/#!forum/android-wear-developer-preview">join + the preview group in Google Groups</a>. + </p> + + <h3> + Opt in for beta testing + </h3> + + <p> + On the <a href= + "https://play.google.com/apps/testing/com.google.android.wearable.app">Testing + Opt-in</a> page, select <strong>Become a Tester</strong>. + </p> + + <h3 id="download-and-install-the-beta-version-of-the-companion-app"> + Download and install the beta version of the companion app + </h3> + + <p> + On the Play Store on your phone, go to the <a href= + "https://play.google.com/store/apps/details?id=com.google.android.wearable.app"> + Android Wear app listing</a>. Tap <strong>Update</strong> to download and + install the beta version of the app. After installation, confirm that + <strong>Auto-update</strong> is selected for the app (see + the "Set up automatic updates for specific apps" section of <a href= + "https://support.google.com/googleplay/answer/113412">Update downloaded + apps</a>). Tap <strong>Open</strong> to start the app. + </p> + + <h3 id="pairing"> + Pair the phone to the watch + </h3> + + <p> + After you install the beta version of the companion app on a phone, + you can pair the phone to the watch: + </p> + + <ol> + <li>On the phone, select your device name from the list of devices. + A pairing code is displayed on the phone and on the watch. + Ensure that the codes match. + </li> + + <li>Tap <strong>Pair</strong> to + continue the pairing process. When the watch is connected to + the phone, a confirmation message is displayed. + On the phone, a screen is displayed that lists + the accounts on the phone. + </li> + + <li>Choose a Google Account to add and sync to your watch. + </li> + + <li>Confirm the screen lock and enter the password to start the copying of + the account from the phone to the watch. + </li> + + <li>Follow the instructions in the wizard to finish the + pairing process. + </li> + </ol> + + <p> + You can begin testing your app with the preview. + </p> + <h2 id="set_up_an_emulator"> Set Up an Emulator </h2> <p> - To test with the Android Emulator, create a virtual device in Android - Studio as follows: + To test with the Android Emulator, + confirm that you have the latest version of the <strong>Android SDK + Platform-tools</strong> from the <a href= + "{@docRoot}studio/intro/update.html#sdk-manager">SDK Manager</a>. + </p> + + <p>Create a new virtual device in Android Studio as follows: </p> <ol> @@ -556,19 +664,19 @@ This is the Android Wear SDK Preview License Agreement (the “License Agreement <li>Click <strong>Create Virtual Device</strong>. </li> - <li>In the <strong>Category</strong> pane, select Wear and - choose a hardware profile. + <li>In the <strong>Category</strong> pane, select <strong>Wear</strong> + and choose a hardware profile. The Android Wear 2.0 Developer Preview is only optimized for round devices currently, so we recommend not using the square or chin profiles for now. Click <strong>Next</strong>. </li> - <li>Select an <strong>N</strong> image to download. The images may be on + <li>Select a <strong>Nougat</strong> image to download. The images may be on the <strong>x86</strong> tab instead of the <strong>Recommended</strong> tab, until installed. For example, select the image with the - <strong>Release Name</strong> of N, the <strong>API Level</strong> of N, - and the <strong>Target</strong> of "Android 6.X (with Android Wear)". + <strong>Release Name</strong> of Nougat, the <strong>API Level</strong> of 24, + and the <strong>Target</strong> of "Android 7.0 (with Android Wear)". When the download and installation are complete, click <strong>Finish</strong> and then click <strong>Next</strong>. </li> @@ -576,16 +684,66 @@ This is the Android Wear SDK Preview License Agreement (the “License Agreement <li>Verify the configuration of the Android Virtual Device (AVD) and click <strong>Finish</strong>. </li> + + <li>Start the emulator by selecting the new virtual device, clicking the + <strong>Play</strong> button, and waiting until + the emulator initializes and shows the Android Wear home screen. + </li> + </ol> + + <p> + Pair the phone with the emulator, and sync a Google Account, as follows: + </p> + + <ol> + <li>On the phone, install the Android Wear app from Google Play. + </li> + + <li>On the phone, enable Developer Options and USB Debugging. + </li> + + <li>Connect the phone to your computer through USB. + </li> + + <li>Forward the AVD's communication port to the connected handheld device + (each time the phone is connected):<br> + <code>adb -d forward tcp:5601 tcp:5601</code> + </li> + + <li>On the phone, in the Android Wear app, begin the standard pairing + process. For example, on the Welcome screen, tap the + <strong>Set It Up</strong> button. + Alternatively, if an existing watch already is paired, in the upper-left + drop-down, tap <strong>Add a New Watch</strong>. + </li> + + <li>On the phone, in the Android Wear app, tap the + Overflow button, and then tap + <strong>Pair with Emulator</strong>. + </li> + + <li>Tap the Settings icon. + </li> + + <li>Under Device Settings, tap <strong>Emulator</strong>. + </li> + + <li>Tap <strong>Accounts</strong> and select a Google Account, + and follow the steps in the wizard to + sync the account with the emulator. If necessary, type the screen-lock + device password, and Google Account password, to start the account sync. + </li> </ol> <p> - You can now test an application with a virtual preview device + You can now test an app with a virtual preview device in the <a href= "{@docRoot}tools/devices/emulator.html">Android Emulator</a>. For more information about using virtual devices, see <a href= - "{@docRoot}tools/devices/managing-avds.html">Managing AVDs with the AVD - Manager</a>. + "{@docRoot}tools/devices/managing-avds.html"> + Create and Manage Virtual Devices</a>. </p> + </div><!-- landing --> </div><!-- relative wrapper --> diff --git a/docs/html/wear/preview/features/app-distribution.jd b/docs/html/wear/preview/features/app-distribution.jd new file mode 100644 index 000000000000..319efa6b6554 --- /dev/null +++ b/docs/html/wear/preview/features/app-distribution.jd @@ -0,0 +1,325 @@ +page.title=App Distribution +meta.keywords="wear-preview" +page.tags="wear-preview" +page.image=images/cards/card-n-sdk_2x.png +@jd:body + +<div id="qv-wrapper"> +<div id="qv"> + +<h2>In this document</h2> + + <ul> + <li><a href="#publish">Publish Your APKs</a></li> + <li><a href="#targeting">Setting Up Targeting for a Watch</a></li> + <li><a href="#console">Using the Play Developer Console</a></li> + </ul> + +</div> +</div> + + <p> + With Android Wear 2.0, a user can visit the Play Store on a watch and + download a Wear app directly to the watch. + </p> + + <p> + Generally, a Wear 2.0 app in the Play Store needs + a minimum and target API level of 24 or higher in + the Android manifest file. The minimum SDK level can be 23 + only if you are using the same APK + for Wear 1.0 and 2.0 (and thus have an embedded Wear 1.0 APK). + </p> + + <h2 id="publish"> + Publish Your APKs + </h2> + + <p> + To make your app appear in the on-watch Play Store, upload + the watch APK in the Play Developer Console just as you would any other + APK. If you only have a watch APK and no phone APK, no other steps + are required. + </p> + + <p> + If you have a phone APK in addition to a watch APK, you must use the + <a href="https://developer.android.com/google/play/publishing/multiple-apks.html">Multi-APK delivery method</a>. + </p> + + <p> + <a href= + "https://developer.android.com/training/permissions/requesting.html">Run-time + permissions</a> are required. + </p> + + <p> + Also see + <a href="{@docRoot}wear/preview/features/standalone-apps.html"> + Standalone Apps</a>. + </p> + + <h3> + Distribution to Wear 2.0 watches + </h3> + + <p> + If you only want your app to be distributed to Wear 2.0 watches, + it is unnecessary to embed the watch APK inside the the phone APK. + </p> + + <p> + If you want your app to + be distributed to Wear 1.0 watches, you need to embed the + watch APK inside the phone APK, as described directly below. + </p> + + <h3> + Distribution to Wear 1.0 and 2.0 watches + </h3> + + <p> + If you are already distributing your app to Wear 1.0 watches, + follow these steps: + </p> + + <ol> + <li>Provide a Wear 2.0 (standalone) version of your watch APK that can be made + available in the Play Store on Wear. + </li> + + <li>Continue embedding a Wear 1.0 APK in your phone APK, + for use by watches that do not have Wear 2.0. + </li> + </ol> + + <h3> + Specifying a version code + </h3> + + <p> + To ensure that a standalone APK acts as an upgrade to an embedded Wear APK, the + standalone Wear APK's <a href= + "https://developer.android.com/google/play/publishing/multiple-apks.html#VersionCodes"> + version code</a> generally should be higher than the embedded Wear APK's version code. + (A phone APK's version code scheme can be independent from that of a watch + APK, although they must be unique.) However, the version codes + of the standalone APK and the embedded Wear APK can be the same if + the APKs are equivalent. If the APKs are not equivalent, + but the version code is the same, then when a watch updates from Wear 1.0 + to 2.0, the watch may get the new APK only after waiting for a + longer-than-expected period of time. + </p> + + <p> + Note that it currently is not possible to create a single APK that works + on a phone and watch. + </p> + + <h3> + Support in the Gradle file + </h3> + + <p> + If you have a Wear app that is intended for both Wear 1.0 and Wear 2.0, + consider using <a href= + "https://developer.android.com/studio/build/build-variants.html#product-flavors"> + product flavors</a>. For example, + if you want to target both SDK version 23 and version 24, + update your Wear module's <code>build.gradle</code> file to include + the following if an existing embedded app has a minimum SDK version of 23: + </p> + +<pre> +android { + ... + defaultConfig + { + // This is the minSdkVersion of the Wear 1.0 embedded app + minSdkVersion 23 + ... + } + buildTypes {...} + productFlavors { + wear1 { + // Use the defaultConfig value + } + wear2 { + minSdkVersion 24 + } + } +</pre> + + <p> + Then update your phone module’s <code>build.gradle</code> file, replacing + <code>wearApp</code> as follows: + </p> + +<pre> +dependencies { + ... + wearApp project(path: ':wearable', configuration: 'wear1Release') +} +</pre> + + <p> + A <a href= + "https://developer.android.com/studio/build/build-variants.html#product-flavors"> + build variant</a> is a combination of the product flavor and build type. + In Android Studio, select the appropriate build variant when + debugging or publishing your app. For example, if <code>wear2</code> is a + product flavor, select <strong>wear2Release</strong> as the + release build variant. + </p> + + <p> + For purposes of code that is Wear 2.0-specific or Wear 1.0-specific, + consider <a href= + "https://developer.android.com/studio/build/build-variants.html#sourcesets"> + source sets for build variants</a>. + </p> + + + <h2 id="targeting"> + Setting Up Targeting for a Watch + </h2> + + <p> + In your Android Manifest file, you must specify the following feature + restriction: the <code>uses-feature</code> element is set to + <code>android.hardware.type.watch</code>. Do not set + the <code>required</code> attribute to <code>false</code>. + A single APK for Wear and non-Wear devices presently is not supported. + </p> + + <p> + Thus, if an APK has the following setting, Google Play provides the APK + to watches only: + </p> + +<pre> +<manifest package="com.example.standalone" + xmlns:android="http://schemas.android.com/apk/res/android"> + <uses-feature + android:name="android.hardware.type.watch" + ... +</manifest> +</pre> + + <p> + The <code>android.hardware.type.watch</code> setting above can be + combined with other criteria such as SDK version, screen resolution, and + CPU architecture. Thus, different Wear APKs can target different hardware + configurations. + </p> + + <h2 id="console"> + Using the Play Developer Console + </h2> + + <p> + Below is an introduction to <a href= + "https://support.google.com/googleplay/android-developer/answer/113469">uploading</a> + a standalone Wear APK to an application listing using the Play Developer + Console. + </p> + + <p> + If your app supports both Wear 1.0 and Wear 2.0, continue embedding the + Wear 1.0 APK (minimum SDK version of 20, 21, or 22, or 23) in the phone + APK and upload the phone APK. In addition, upload your standalone Wear + 2.0 APK (which has a minimum SDK version of 24). + </p> + + <p> + Also see <a href= + "https://developer.android.com/google/play/publishing/multiple-apks.html"> + Multiple APK Support</a> and <a href= + "https://developer.android.com/distribute/googleplay/developer-console.html#manage"> + Manage Your App</a>. + Before uploading an APK as described below, the APK + must be <a href= + "https://developer.android.com/studio/publish/app-signing.html#release-mode"> + signed</a>. + </p> + + <h3 id="uploading-apk"> + Uploading your APK + </h3> + + <p> + Go to the <a href="https://play.google.com/apps/publish">Play Developer + Console</a>, navigate to your application listing, and select + <strong>APK</strong> in the left-navigation panel. An APK screen similar to + the following is displayed: + </p> + <img src="../images/apk-tabs.png" width="" alt="alt_text"> + + <p> + You may need to use advanced mode for uploads, as follows: + </p> + + <ul> + <li>Advanced mode is unnecessary if you only have a Wear 2.0 app and no + phone app. Instead of advanced mode, use simple mode.</li> + + <li>Use advanced mode if you support Wear 1.0 or have a phone app.</li> + </ul> + + <p> + Therefore, on the above APK screen, to determine whether to click + the <strong>Switch to advanced mode</strong> + button, consider the following: + </p> + + <ul> + <li>If your app does not support Wear 1.0, and only has a watch APK, + upload it using simple mode. + </li> + + <li>If your app does not support Wear 1.0 and has both a watch APK and a + phone APK, click <strong>Switch to advanced mode</strong> + to upload the watch and phone APKs. + </li> + </ul> + + <p> + See <a href= + "https://developer.android.com/google/play/publishing/multiple-apks.html#SimpleAndAdvanced"> + Simple mode and advanced mode</a> for more information about toggling + between modes. + </p> + + <p> + Select the appropriate tab (<strong>Production</strong>, <strong>Beta + Testing</strong>, or <strong>Alpha Testing</strong>) for your upload. + Then click + the <strong>Upload New APK</strong> button and select your standalone + Wear APK for upload. + </p> + + <h3> + Reviewing and publishing + </h3> + + <p> + After you upload your standalone Wear APK and scroll down the resulting + page, the APK is shown in the <strong>Current APK</strong> table, with a + version number, in a similar way to the following: + </p> + <img src="../images/current-apk.png" width="" alt="alt_text"> + + <p> + Finally, in the <strong>Current APK</strong> table above, click the line + with the <strong>Version</strong> to review the APK. The <strong>APK + Details</strong> panel is displayed. You can verify, for example, that + the number in the <strong>Supported Android Devices</strong> line is far + fewer than the number would be for a typical phone APK: + </p> + <img src="../images/apk-details.png" width="" alt="alt_text"> + + <p> + When you are ready, <a href= + "https://support.google.com/googleplay/android-developer/answer/6334282">publish</a> + your app. + </p> diff --git a/docs/html/wear/preview/features/bridger.jd b/docs/html/wear/preview/features/bridger.jd index b7be093ed298..2d879caeccc2 100644 --- a/docs/html/wear/preview/features/bridger.jd +++ b/docs/html/wear/preview/features/bridger.jd @@ -6,19 +6,26 @@ page.tags="wear-preview" <div id="qv-wrapper"> <div id="qv"> - <ol> + <ul> <li> <a href= - "#preventing_bridging_with_the_bridging_mode_feature">Preventing - Bridging with the Bridging Mode Feature</a> + "#using-an-entry-in-the-manifest-file">Specifying a Bridging Configuration in the Manifest File</a> </li> <li> <a href= - "#using_a_dismissal_id_to_sync_notification_dismissals">Using a - Dismissal ID to Sync Notification Dismissals</a> + "#specifying-a-bridging-configuration-at-runtime">Specifying a Bridging Configuration at Runtime</a> </li> - </ol> + <li> + <a href= + "#existing-method-of-preventing-bridging">Existing Method of Preventing Bridging</a> + </li> + + <li> + <a href= + "#using_a_dismissal_id_to_sync_notification_dismissals">Using a Dismissal ID to Sync Notification Dismissals</a> + </li> + </ul> </div> </div> @@ -27,19 +34,20 @@ page.tags="wear-preview" "{@docRoot}training/wearables/notifications/index.html">are bridged (shared)</a> from an app on a companion phone to the watch. If you build a standalone watch app and have a companion phone app, they may duplicate - notifications. The Android Wear 2.0 Preview includes a Bridging mode - feature to handle this problem of repeated notifications. + notifications. The Android Wear 2.0 Preview includes + features to handle this problem of repeated notifications. </p> <p> - With the Android Wear 2.0 Preview, developers can change the - behavior of notifications with the following: + With the Android Wear 2.0 Preview, developers can change the behavior of + notifications with one or more of the following: </p> <ul> - <li>Specifying in the standalone app's Android manifest file that - notifications from the corresponding phone app should not be - bridged to the watch + <li>Specifying a bridging configuration in the manifest file + </li> + + <li>Specifying a bridging configuration at runtime </li> <li>Setting a dismissal ID so notification dismissals are synced across @@ -47,43 +55,201 @@ page.tags="wear-preview" </li> </ul> - <h2 id="preventing_bridging_with_the_bridging_mode_feature"> - Preventing Bridging with the Bridging Mode Feature + <h2 id="using-an-entry-in-the-manifest-file"> + Specifying a Bridging Configuration in the Manifest File </h2> <p> - To prevent bridging of notifications from a phone app, you can use an + An app's Android manifest file can indicate that notifications from the + corresponding phone app should not be bridged to the watch. Specifically, + to prevent bridging of notifications from a phone app, you can use a + <code><meta-data></code> entry in the manifest file of the watch app (e.g. the standalone watch app), as follows: </p> - <pre> +<pre> com.google.android.wearable.notificationBridgeMode - </pre> +</pre> <p> Setting that entry to <code>NO_BRIDGING</code> will prevent bridging: </p> - <pre> -<meta-data android:name="com.google.android.wearable.notificationBridgeMode" - android:value="NO_BRIDGING" /> +<pre> +<meta-data android:name="com.google.android.wearable.notificationBridgeMode" + android:value="NO_BRIDGING" /> </pre> + <p> - The default bridging behavior occurs if you do not include the entry or + The default bridging behavior occurs if you do not + include the <code><meta-data></code> entry or if you specify a value of <code>BRIDGING</code> instead of <code>NO_BRIDGING</code>. </p> - <h3 id="existing_method_of_preventing_bridging"> - Existing method of preventing bridging + <p> + For an existing app, if you are using + Google Cloud Messaging (GCM) or Firebase Cloud + Messaging (FCM) to send notification alerts to devices, + you may already have disabled bridging in case a phone is not + connected at the time of receiving an alert. + In this case, you may still want to dismiss the notification + across other devices when it is dismissed in a watch app. + </p> + + <p> + The bridging configuration that is set in the manifest takes effect as + soon as a watch app is installed. + </p> + + <h2 id="specifying-a-bridging-configuration-at-runtime"> + Specifying a Bridging Configuration at Runtime + </h2> + + <p> + This section describes how to specify a bridging configuration at runtime + using the <code>BridgingManager</code> class + <code>(android.support.wearable.notifications.BridgingManager)</code>. + </p> + + <p> + You can set a bridging mode, and optionally set tags for notifications + that are exempt from the bridging mode, using a + <code>BridgingManager</code> object. Specifically, create a + <code>BridgingConfig</code> object and set it as shown in this section, + optionally using the <code>setBridgingEnabled</code> method. If you + specify a bridging configuration at runtime, then if the + <code>setBridgingEnabled</code> method is not set, bridging is enabled by + default. + </p> + + <p> + Specifying a bridging configuration at runtime overrides a + bridging-related setting in the Android manifest file. + </p> + + <h3 id="disable-bridging-for-all-notifications"> + Disable bridging for all notifications + </h3> + + <p> + You can use the <code>setBridgingEnabled</code> method, as follows: + </p> + +<pre> +BridgingManager.setConfig(context, + new BridgingConfig.Builder(context) + .setBridgingEnabled(false) + .build()); +</pre> + <p> + If the above setter is not called, the bridging mode defaults to true. + Here is an example of setting tags without using the + <code>setBridgingEnabled</code> method, excluding notifications with a + tag of <code>foo</code> or <code>bar</code>: + </p> + +<pre> +BridgingManager.setConfig(context, + new BridgingConfig.Builder(context) + .addExcludedTag("foo") + .addExcludedTag("bar") + .build()); +</pre> + <h3 id="exempt-notifications-that-are-tagged"> + Exempt notifications that are tagged </h3> <p> + You can disable bridging for all notifications except those with certain + tags. + </p> + + <p> + For example, you can disable bridging, except for notifications tagged as + <code>foo</code> or <code>bar,</code> with the following: + </p> + +<pre> +BridgingManager.setConfig(context, + new BridgingConfig.Builder(context) + .setBridgingEnabled(false) + .addExcludedTag("foo") + .addExcludedTag("bar") + .build()); +</pre> + + <p> + As another example, you can disable bridging for all notifications except + for notifications tagged as <code>foo</code>, <code>bar</code> or + <code>baz</code>. + </p> + + <pre> +BridgingManager.setConfig(context, + new BridgingConfig.Builder(context) + .setBridgingEnabled(false) + .addExcludedTags(Arrays.asList("foo", "bar", "baz")) + .build()); +</pre> + <h3 id="enable-bridging-except-for-notifications-with-certain-tags"> + Enable bridging except for notifications with certain tags + </h3> + + <p> + You can enable bridging for all notifications except those with certain + tags. + </p> + + <p> + For example, you can enable bridging for all notifications, except for + notifications tagged as <code>foo</code> or <code>bar</code>, with the + following: + </p> + +<pre> +BridgingManager.setConfig(context, + new BridgingConfig.Builder(context) + .setBridgingEnabled(true) + .addExcludedTag("foo") + .addExcludedTag("bar") + .build()); +</pre> + + <h3 id="setting-a-bridge-tag"> + Setting a bridge tag + </h3> + + <p> + A bridge tag can be set on a notification by calling the + <code>setNotificationBridgeTag</code> method as follows: + </p> + +<pre> +BridgingManager.setNotificationBridgeTag(<NotificationCompat.Builder>, <String>); +</pre> + + <p> + For example: + </p> + +<pre> +NotificationCompat.Builder builder = new NotificationCompat.Builder(context) +<set other fields>; +BridgingManager.setNotificationBridgeTag(builder, "foo"); +Notification notification = builder.build(); +</pre> + + <h2 id="existing-method-of-preventing-bridging"> + Existing Method of Preventing Bridging + </h2> + + <p> An existing way to prevent bridging is with the <code>Notification.Builder</code> class; specify <code>true</code> in the <a href= - "{@docRoot}reference/android/app/Notification.Builder.html#setLocalOnly(boolean)"> + "http://developer.android.com/reference/android/app/Notification.Builder.html#setLocalOnly(boolean)"> setLocalOnly</a> method. </p> @@ -95,12 +261,6 @@ com.google.android.wearable.notificationBridgeMode the watch app may not be installed on all of them. </p> - <p> - Thus, if bridging should be prevented when the watch app - is installed, use the <a href= - "#preventing_bridging_with_the_bridging_mode_feature">Bridging mode - feature</a>. - </p> <h2 id="using_a_dismissal_id_to_sync_notification_dismissals"> Using a Dismissal ID to Sync Notification Dismissals @@ -110,7 +270,7 @@ com.google.android.wearable.notificationBridgeMode If you prevent bridging with the Bridging mode feature, dismissals (cancellations) of notifications are not synced across a user's devices. However, the following methods of the <a href= - "{@docRoot}reference/android/support/v4/app/NotificationCompat.WearableExtender.html"> + "http://developer.android.com/reference/android/support/v4/app/NotificationCompat.WearableExtender.html"> NotificationCompat.WearableExtender</a> class enable you to use dismissal IDs: </p> @@ -118,7 +278,7 @@ com.google.android.wearable.notificationBridgeMode <pre> public WearableExtender setDismissalId(String dismissalId) public String getDismissalId() - </pre> +</pre> <p> To enable a dismissal to be synced, use the <code>setDismissalId()</code> method. For each notification, pass a globally unique ID, as a string, @@ -135,12 +295,12 @@ public String getDismissalId() <pre> NotificationCompat.WearableExtender wearableExtender = -new NotificationCompat.WearableExtender().setDismissalId(“abc123”); +new NotificationCompat.WearableExtender().setDismissalId("abc123"); Notification notification = new NotificationCompat.Builder(context) <set other fields> .extend(wearableExtender) .build(); - </pre> +</pre> <p> Dismissal IDs work if a watch is paired to an Android phone, but not if a watch is paired to an iPhone. diff --git a/docs/html/wear/preview/features/complications.jd b/docs/html/wear/preview/features/complications.jd index 3334cb79b143..c8661188eb0a 100644 --- a/docs/html/wear/preview/features/complications.jd +++ b/docs/html/wear/preview/features/complications.jd @@ -13,6 +13,13 @@ page.image=/wear/preview/images/complications-main-image.png Complications to a Watch Face</a> </li> <li> + <a href="#permissions-for-complication-data">Permissions + for Complication Data</a> + </li> + <li> + <a href="#default-providers">Default Providers for Watch Faces</a> + </li> + <li> <a href="#exposing_data_to_complications">Exposing Data to Complications</a> </li> @@ -27,12 +34,14 @@ page.image=/wear/preview/images/complications-main-image.png <a href="#api_additions">API Additions</a> </li> </ol> + <h2>See Also</h2> <ol> <li><a class="external-link" href="https://github.com/googlesamples/android-WatchFace">Watch Face sample app with complications</a></li> </ol> + </div> </div> @@ -56,9 +65,12 @@ page.image=/wear/preview/images/complications-main-image.png </p> <p> - Along with reviewing this page, download the Android Wear 2.0 Preview - Reference (see the Complications API <a href= - "#api_additions">additions</a>) and review the Javadoc for complications. + You can review the Javadoc for complications by downloading + the Android Wear 2.0 Preview + Reference. Also see the <a href="#api_additions">API additions for + complications</a> and the + <a href="https://developer.android.com/wear/preview/behavior-changes.html"> + behavior changes</a> for Wear 2.0. </p> <p> @@ -117,8 +129,8 @@ page.image=/wear/preview/images/complications-main-image.png <code>WatchFaceService.Engine</code> class, with a list of watch face complication IDs. A watch face creates these IDs to uniquely identify slots on the watch face where complications can appear, and passes them - to the <code>createProviderChooserIntent</code> method (of the - <code>ProviderChooserIntent</code> class) to allow the user to decide + to the <code>createProviderChooserIntent</code> method + to allow the user to decide which complication should go in which slot. </p> @@ -186,6 +198,406 @@ page.image=/wear/preview/images/complications-main-image.png where possible. </p> + <h2 id="permissions-for-complication-data"> + Permissions for Complication Data + </h2> + + <p> + A watch face must have the following <a href= + "https://developer.android.com/training/permissions/requesting.html">permission</a> + to receive complication data and open the provider chooser: + </p> + +<pre> +com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA +</pre> + + <h3 id="opening-the-provider-chooser"> + Opening the provider chooser + </h3> + + <p> + A watch face that was not granted the above permission will be unable to + start the provider chooser. + </p> + + <p> + To make it easier to request the permission and start the chooser, the + <code>ComplicationHelperActivity</code> class is available in the + wearable support library. This class should be used instead of + <code>ProviderChooserIntent</code> to start the chooser in almost all + cases. + </p> + + <h4 id="requesting-the-necessary-permission"> + Requesting the necessary permission + </h4> + + <p> + To use <code>ComplicationHelperActivity</code>, add it to the watch face + in the <a href= + "https://developer.android.com/guide/topics/manifest/manifest-intro.html"> + manifest file</a>: + </p> + +<pre> +<activity android:name="android.support.wearable.complications.ComplicationHelperActivity"/> +</pre> + + <p> + To start the provider chooser, call the + <code>ComplicationHelperActivity.createProviderChooserHelperIntent</code> + method, to obtain an intent. + </p> + + <p> + The new intent can be used with either <code>startActivity</code> or + <code>startActivityForResult</code> to launch the chooser. + </p> + + <p> + Here is an example of using the new intent with + <code>startActivityForResult</code>: + </p> + + <pre> +startActivityForResult( + ComplicationHelperActivity.createProviderChooserHelperIntent( + getActivity(), + watchFace, + complicationId, + ComplicationData.TYPE_LARGE_IMAGE), + PROVIDER_CHOOSER_REQUEST_CODE); +</pre> + <p> + When the helper activity is started, the helper activity checks if the + permission was granted. If the permission was not granted, the helper + activity makes a runtime permission request. If the permission request is + accepted (or is unneeded), the provider chooser is shown. + </p> + + <p> + If <code>startActivityForResult</code> was used with the intent, the + result delivered back to the calling Activity will have a result code of + <code>RESULT_OK</code> if a provider was successfully set, or a result + code of <code>RESULT_CANCELLED</code> if no provider was set. + </p> + + <p> + In the case where a provider was set, + <code>ComplicationProviderInfo</code> for the chosen provider will be + included in the data intent of the result, as an extra with the key + <code>ProviderChooserIntent#EXTRA_PROVIDER_INFO</code>. + </p> + + <h3 id="receiving-complication-data"> + Receiving complication data + </h3> + + <p> + In general, watch faces need the above permission in order to receive + complication data, but there are some exceptions. Specifically, a watch + face can only receive data from a provider if one of the following is + true: + </p> + + <ul> + <li>The provider is a "safe" system provider, + </li> + + <li>The provider and watch face are from the same app, + </li> + + <li>The provider whitelists the watch face as a "safe" watch face, or + </li> + + <li>The watch face has the permission + </li> + </ul> + + <h4 id="lack-of-appropriate-permission"> + Lack of appropriate permission + </h4> + + <p> + If none of the above is true, then when <code>ComplicationData</code> + normally would be sent by a provider to a watch face, the system instead + sends data of the type <code>TYPE_NO_PERMISSION</code>. This type + includes an icon (an exclamation mark) and short text ("--") to allow it + to be rendered as if it were of the short text type or icon type, for + convenience. + </p> + + <p> + When a watch face receives data of <code>TYPE_NO_PERMISSION</code>, the + watch face should render this appropriately, so the user can see that + action is needed for the complication to work. If possible, a tap on a + complication in this state should launch a permission request. This can + be done using + <code>ComplicationHelperActivity.createPermissionRequestHelperIntent</code>, + if the helper activity was added to the watch face app. + </p> + + <p> + If a user accepts the permission request created by the helper activity, + updates are requested for all the active complications on the watch face + automatically, allowing the <code>TYPE_NO_PERMISSION</code> data to be + replaced by real data. + </p> + + <h4 id="safe-providers"> + Safe providers + </h4> + + <p> + Some system providers are considered "safe", because they only supply + information that the watch face already could obtain itself. + </p> + + <p> + These providers are listed in the new <code>SystemProviders</code> class + in the wearable support library. Whether a system provider is safe is + stated in the Javadoc (in the Android Wear 2.0 Preview Reference). Also + see <a href="#system-providers">System providers</a> for a list. + </p> + + <h4 id="provider-specified-safe-watch-faces"> + Provider-specified safe watch faces + </h4> + + <p> + Providers can specify certain watch faces as "safe" to receive their + data. This is intended to be used only when the watch face will attempt + to use the provider as a default (see below), + and the provider trusts the watch face app. + </p> + + <p> + To declare watch faces as safe, the provider adds metadata with a key of + <code>android.support.wearable.complications.SAFE_WATCH_FACES</code>. The + metadata value should be a comma-separated list (whitespace is ignored). + Entries in the list can be component names (of + <code>WatchFaceServices</code>, given as if + <code>ComponentName.flattenToString()</code> had been called), or they + can be package names (of apps, in which case every watch face within a + specified app is considered safe). + </p> + + <p> + For example: + </p> + +<pre> +<meta-data + android:name="android.support.wearable.complications.SAFE_WATCH_FACES" + android:value=" + com.app.watchface/com.app.watchface.MyWatchFaceService, + com.anotherapp.anotherwatchface/com.something.WatchFaceService, + com.something.text + "/> +</pre> + + <h2 id="default-providers"> + Default Providers for Watch Faces + </h2> + + <p> + Watch faces can specify default providers that are used until a user + selects a provider. + </p> + + <h3 id="setting-default-providers"> + Setting default providers + </h3> + + <p> + Set default providers using the + <code>setDefaultComplicationProvider</code> method in + <code>WatchFaceService.Engine</code>. This method may be called at any + time, but it does nothing if the user already chose a provider for the + given complication. + </p> + + <h3 id="safe-providers2"> + Safe providers + </h3> + + <p> + For most providers, the <code>RECEIVE_COMPLICATION_DATA</code> permission + must be granted to a watch face before data can flow to it. However, some + system providers are considered "safe", and do not require the watch face + to have the permission for data to be sent (see <a href= + "#safe-providers">Safe Providers</a> and <a href= + "#system-providers">System providers</a>). These providers may be + preferable to use as defaults, as they can supply data immediately. + </p> + + <p> + Alternatively, if a watch face has a partnership with a certain provider + and wishes to use it as a default, it can request that the provider list + it as a safe watch face (see <a href= + "#provider-specified-safe-watch-faces">Provider-specified safe watch + faces</a>). + </p> + + <h3 id="system-providers"> + System providers + </h3> + + <p> + The system includes providers that can be used as defaults. These are + listed in the <code>SystemProviders</code> class in the wearable support + library. + </p> + + <p> + The following table has details about providers that are considered safe: + </p> + + <table> + <tr> + <th> + Method name in the SystemProviders class + </th> + <th> + Safety + </th> + <th> + Can be the default + </th> + <th> + Notes + </th> + </tr> + + <tr> + <td> + <code>dateProvider()</code> + </td> + <td> + Yes + </td> + <td> + Yes + </td> + <td> + The standard system date provider. Tapping opens the standard Agenda + app. + </td> + </tr> + + <tr> + <td> + <code>currentTimeProvider()</code> + </td> + <td> + Yes + </td> + <td> + Yes + </td> + <td> + The standard system "time and date" provider. No tap action. + </td> + </tr> + + <tr> + <td> + <code>batteryProvider()</code> + </td> + <td> + Yes + </td> + <td> + Yes + </td> + <td> + The standard system battery provider. No tap action. + </td> + </tr> + + <tr> + <td> + <code>stepCountProvider()</code> + </td> + <td> + Yes + </td> + <td> + Yes + </td> + <td> + Shows a daily total of steps, as reported by + <code>readDailyTotal</code>. + </td> + </tr> + + <tr> + <td> + <code>unreadCountProvider()</code> + </td> + <td> + Yes + </td> + <td> + Yes + </td> + <td> + Shows the number of unread notifications in the stream. + </td> + </tr> + + <tr> + <td> + <code>worldClockProvider()</code> + </td> + <td> + Yes + </td> + <td> + Yes + </td> + <td> + Will default to London or New York. Can be tapped to change the time + zone. + </td> + </tr> + + <tr> + <td> + <code>appsProvider()</code> + </td> + <td> + Yes + </td> + <td> + Yes + </td> + <td> + Will show an "apps" icon at first, which can be tapped to choose an + app. + </td> + </tr> + + <tr> + <td> + <code>nextEventProvider()</code> + </td> + <td> + No + </td> + <td> + Yes (but not a safe provider) + </td> + <td> + The standard system "next event" provider. Tapping opens + the standard Agenda app. + </p> + </td> + </tr> + </table> + + <h2 id="exposing_data_to_complications"> Exposing Data to Complications </h2> @@ -203,6 +615,11 @@ page.image=/wear/preview/images/complications-main-image.png be used to send data back to the system. </p> + <p class="note"><strong>Note:</strong> When you provide data as a + complication data provider, the watch face receives the raw values + you send so it can draw them on the watch face. + </p> + <p> In your app's manifest, declare the service and add an intent filter for the following: @@ -210,7 +627,8 @@ page.image=/wear/preview/images/complications-main-image.png <pre> android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST -</pre> + </pre> + <p> The service's manifest entry should also include an <code>android:icon</code> attribute. The provided icon should be a @@ -227,6 +645,21 @@ android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST <a href="#api_additions">API Additions</a>). </p> + <p> + Additionally, a permission for provider services ensures that only the Android Wear system + can bind to provider services. Only the Android Wear system can have this + permission. + </p> + + <p> + Provider services should add the following to their service declarations + in the manifest: + </p> + +<pre> +android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER" +</pre> + <h3 id="update_period"> Update period </h3> @@ -371,6 +804,11 @@ METADATA_KEY_PROVIDER_CONFIG_ACTION <p> The following table describes the types and fields of the <code>ComplicationData</code> object. + If a watch face requests a field that is invalid for a complication type, + a default value for the field is returned. + For example, if a watch face tries to access a <code>Long text</code> + field in a <code>SHORT_TEXT</code> type, the default value for the + <code>Long text</code> field is returned. </p> <table> @@ -489,56 +927,80 @@ METADATA_KEY_PROVIDER_CONFIG_ACTION </table> <p> - In addition, the following two types have no fields. These two types may - be sent for any complication slot and do not need to be included in a - list of supported types: + In addition, the types in the table below are for empty data and + may be sent for any complication slot. These types have no fields + and do not need to be included in a + list of supported types. These types enable watch + faces to differentiate among the following three cases: + </p> + + <ul> + <li>No provider was chosen + </li> + + <li>The user has selected "empty" for a slot + </li> + + <li>A provider has no data to send + </li> + </ul> + + <p> + Providers should not send <code>TYPE_EMPTY</code> in response to + update requests. Providers should send <code>TYPE_NO_DATA</code> instead. + </p> + + <p> + Details on the complication types for "empty" data are in the + following table: </p> <table> <tr> - <th style="width:175px"> - Type - </th> - <th style="width:175px"> - Required fields + <th>Complication type </th> - <th style="width:175px"> - Optional fields - </th> - <th> - Notes + <th>Description </th> </tr> <tr> <td> - NOT_CONFIGURED - </td> - <td> - None - </td> - <td> - None + <code>TYPE_NOT_CONFIGURED</code> </td> <td> - Sent when a provider has not yet been chosen for a complication. + Sent by the system when a complication is activated but the user has + not selected a provider, and no default was set. + <p> + Cannot be sent by providers. + </p> </td> </tr> <tr> <td> - EMPTY + <code>TYPE_EMPTY</code> </td> <td> - None + Sent by the system when a complication is activated and the user has + chosen "empty" instead of a provider, or when the watch face has + chosen no provider, and this type, as the default. + <p> + Cannot be sent by providers. + </p> </td> + </tr> + + <tr> <td> - None + <code>TYPE_NO_DATA</code> </td> <td> - Sent by a provider when there is no data to display in a - complication, or sent by the system when nothing should be shown in a - complication. + Sent by the system when a complication (that has a provider) is + activated, to clear the complication before actual data is received + from the provider. + <p> + Should be sent by providers if they have no actual data to send. + </p> </td> </tr> </table> @@ -700,8 +1162,8 @@ METADATA_KEY_PROVIDER_CONFIG_ACTION </h2> <p> - The Complications API includes new classes in the Wearable Support - Library. For more information, download the <a href= + The Complications API includes new classes in the wearable support + library. For more information, download the <a href= "{@docRoot}wear/preview/start.html#get_the_preview_reference_documentation"> Android Wear 2.0 Preview Reference</a>. </p> @@ -722,14 +1184,26 @@ METADATA_KEY_PROVIDER_CONFIG_ACTION </li> <li> - <code>ComplicationText</code> + <code>ComplicationHelperActivity</code> <ul> - <li>Used to supply text-based values in a - <code>ComplicationData</code> object + <li>Used to request the following permission: <br> +<code>com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA</code> </li> - <li>Includes options for time-dependent values, whose text value - depends on the current time + <li>Used instead of <code>ProviderChooserIntent</code> + to start the chooser in almost all cases + </li> + </ul> + </li> + + <li> + <code>ComplicationManager</code> + <ul> + <li>A wrapper for the complication manager service, for use by + providers + </li> + + <li>Allows providers to send complication data to the system </li> </ul> </li> @@ -747,13 +1221,14 @@ METADATA_KEY_PROVIDER_CONFIG_ACTION </li> <li> - <code>ComplicationManager</code> + <code>ComplicationText</code> <ul> - <li>A wrapper for the complication manager service, for use by - providers + <li>Used to supply text-based values in a + <code>ComplicationData</code> object </li> - <li>Allows providers to send complication data to the system + <li>Includes options for time-dependent values, whose text value + depends on the current time </li> </ul> </li> @@ -761,11 +1236,8 @@ METADATA_KEY_PROVIDER_CONFIG_ACTION <li> <code>ProviderChooserIntent</code> <ul> - <li>Non-instantiable utility class - </li> - - <li>Includes a method that a watch face can call for starting a - provider chooser (to allow a user to configure complications) + <li>Non-instantiable utility class that is not commonly used; use + <code>ComplicationHelperActivity</code> instead </li> </ul> </li> @@ -789,6 +1261,16 @@ METADATA_KEY_PROVIDER_CONFIG_ACTION </li> </ul> </li> + + <li> + <code>SystemProviders</code> + <ul> + <li>Lists system providers that are considered "safe", + because they only supply information that the watch face + already could obtain itself + </li> + </ul> + </li> </ul> <p> diff --git a/docs/html/wear/preview/features/notifications.jd b/docs/html/wear/preview/features/notifications.jd index dcc09709deb3..b546978a8b9c 100644 --- a/docs/html/wear/preview/features/notifications.jd +++ b/docs/html/wear/preview/features/notifications.jd @@ -1,6 +1,5 @@ page.title=Notification Changes in Android Wear 2.0 -meta.tags="wear", "wear-preview", "notifications" -page.tags="wear" +meta.tags="wear", "wear-preview", "notifications" page.tags="wear" page.image=/wear/preview/images/expanded_diagram.png @@ -12,6 +11,7 @@ page.image=/wear/preview/images/expanded_diagram.png <h2>This document includes</h2> <ol> <li><a href="#visual">Visual Updates</a></li> + <li><a href="#inline">Inline Action</a></li> <li><a href="#expanded">Expanded Notifications</a></li> <li><a href="#messaging">MessagingStyle</a></li> </ol> @@ -67,7 +67,8 @@ material design</a> visual changes. We recommended that you don't set color for bridged notifications. When Wear apps post local notifications, you can work around this by checking - <a href="{@docRoot}training/basics/supporting-devices/platforms.html#version-codes">the API level of the device</a> they're running on and using an appropriate color + <a href="{@docRoot}training/basics/supporting-devices/platforms.html#version-codes">the API level of the device</a> + they're running on and using an appropriate color for Wear 1.x and a different color for Wear 2.0. </li> @@ -77,6 +78,85 @@ material design</a> visual changes. you must update the text of your notification. </li> </ul> + +<h2 id="inline">Inline Action</h3> + +<img src="{@docRoot}wear/preview/images/inline_action.png" style="float:right;margin:10px 20px 0 0"> +<p> + Wear 2.0 now supports inline action, which allows users to take actions on a + notification from within the notification stream card. On Wear, the inline + action appears as an additional button displayed at the bottom of the notification. +</p> +<p> + Inline actions are optional but recommended for cases in which users are likely + to take an action on a notification after viewing the contents in the + notification stream card (without going to the + <a href= "{@docRoot}wear/preview/features/notifications.html#expanded">expanded notification</a>). + Examples of good use cases for inline action on a notification include: replying to a + text message, stopping a fitness activity, and archiving an email message. +</p> + +<p> + A notification can provide only one inline action. + To display the inline action as an additional button in the notification, set + the <a href="https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Action.WearableExtender.html#setHintDisplayActionInline(boolean)">{@code setHintDisplayActionInline()}</a> + method to true. When a user taps the inline action, the system invokes + the intent that you specified in the notification action. +</p> + +<h3>Adding an inline action</h3> +<p> + The following code example shows how to create a notification with an inline + reply action: +</p> + +<ol> + <li>Create an instance of + <a href="https://developer.android.com/reference/android/support/v4/app/RemoteInput.Builder.html">{@code RemoteInput.Builder}</a></code> + that you can add to your notification action. This class's constructor accepts a + string that the system uses as the key for the text input. Later, your app + uses that key to retrieve the text of the input. + +<pre> +String[] choices = context.getResources().getStringArray(R.array.notification_reply_choices); + choices = WearUtil.addEmojisToCannedResponse(choices); + RemoteInput remoteInput = new RemoteInput.Builder(Intent.EXTRA_TEXT) + .setLabel(context.getString(R.string.notification_prompt_reply)) + .setChoices(choices) + .build(); +</pre> + + </li> + + <li> + Use the <a href="https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Action.Builder.html#addRemoteInput(android.support.v4.app.RemoteInput)">{@code addRemoteInput()}</a> + method to attach the <ahref="https://developer.android.com/reference/android/support/v4/app/RemoteInput.html">{@code RemoteInput}</a> + object to an action. + +<pre> +NotificationCompat.Action.Builder actionBuilder = new NotificationCompat.Action.Builder( + R.drawable.ic_full_reply, R.string.notification_reply, replyPendingIntent); + actionBuilder.addRemoteInput(remoteInput); + actionBuilder.setAllowGeneratedReplies(true); +</pre> + </li> + + <li> + Add a hint to display the reply action inline, and use the + <a href="https://developer.android.com/reference/android/support/v4/app/NotificationCompat.WearableExtender.html#addAction(android.support.v4.app.NotificationCompat.Action)">{@code addAction}</a> + method to add this action to the notification. + +<pre> +// Android Wear 2.0 requires a hint to display the reply action inline. + Action.WearableExtender actionExtender = + new Action.WearableExtender() + .setHintLaunchesActivity(true) + .setHintDisplayActionInline(true); + wearableExtender.addAction(actionBuilder.extend(actionExtender).build()); +</pre> + </li> +</ol> + <h2 id="expanded">Expanded Notifications</h2> <p>Android Wear 2.0 introduces <i>expanded notifications</i>, which provide substantial additional content and actions for each notification. @@ -152,51 +232,52 @@ action in the notification unless a different action is specified using </p> <h2 id="messaging">MessagingStyle</h2> -<p>If you have a chat messaging app, your notifications should use -<a href="{@docRoot}preview/features/notification-updates.html#style">{@code Notification.MessagingStyle}</a>, - which is new in Android N. Wear 2.0 uses the chat messages included - in a <a href="{@docRoot}preview/features/notification-updates.html#style">{@code MessagingStyle}</a> notification - - (see <a href="{@docRoot}preview/features/notification-updates.html#style">{@code addMessage()}</a>) to provide - a rich chat app-like experience in the expanded notification. +<p> + If you have a chat messaging app, your notifications should use + <a href="https://developer.android.com/reference/android/support/v4/app/NotificationCompat.MessagingStyle.html">{@code NotificationCompat.MessagingStyle}</a>, + which is new in Android 7.0. Wear 2.0 uses the chat messages included in a + {@code MessagingStyle} notification + (see <a href="https://developer.android.com/reference/android/support/v4/app/NotificationCompat.MessagingStyle.html#addMessage(android.support.v4.app.NotificationCompat.MessagingStyle.Message)">{@code addMessage()}</a>) + to provide a rich chat app-like experience in the expanded notification. </p> -<p class="note">Note: <a href="{@docRoot}preview/features/notification-updates.html#style">{@code MessagingStyle}</a> +<p class="note">Note: <a href="https://developer.android.com/reference/android/support/v4/app/NotificationCompat.MessagingStyle.html">{@code MessagingStyle}</a> expanded notifications require that you have at least version 1.5.0.2861804 of the <a href="https://play.google.com/store/apps/details?id=com.google.android.wearable.app">Android Wear app</a> - on your paired Android phone. That version will be available within the next - few weeks in the Play Store. + on your paired Android phone. </p> <h3 id="smart-reply">Smart Reply</h3> <img src="{@docRoot}wear/preview/images/messaging_style.png" height="420" style="float:right;margin:10px 20px 0 0" /> -<p>Wear 2.0 also introduces <i>Smart Reply</i> -for <a href="{@docRoot}preview/features/notification-updates.html#style">{@code MessagingStyle}</a> notifications. +<p>Wear 2.0 also introduces <i>Smart Reply</i> for + <a href="https://developer.android.com/reference/android/support/v4/app/NotificationCompat.MessagingStyle.html">{@code MessagingStyle}</a> notifications. Smart Reply provides the user with contextually relevant, touchable choices in the expanded notification and in {@code RemoteInput}. These augment the fixed list of choices that the developer provides in - <a href="http://developer.android.com/reference/android/support/v4/app/RemoteInput.html">{@code RemoteInput}</a> - using the - <a href="{@docRoot}reference/android/support/v4/app/RemoteInput.Builder.html#setChoices(java.lang.CharSequence[])">{@code setChoices()}</a> method. + <a href="http://developer.android.com/reference/android/support/v4/app/RemoteInput.html">{@code RemoteInput}</a> + using the + <a href="{@docRoot}reference/android/support/v4/app/RemoteInput.Builder.html#setChoices(java.lang.CharSequence[])">{@code setChoices()}</a> method. </p> -<p>By enabling Smart Reply for your MessagingStyle notifications, - you provide users with a fast (single tap), discreet (no speaking aloud), and - reliable way to respond to chat messages. +<p> Smart Reply provides users with a fast (single tap), discreet (no speaking aloud), + private (messages received by a user never leave the watch), and reliable (no + internet connection needed) way to respond to chat messages. </p> -<p>Responses generated by Smart Reply are shown in addition to those set using the - <a href="{@docRoot}reference/android/support/v4/app/RemoteInput.Builder.html#setChoices(java.lang.CharSequence[])">{@code setChoices()}</a> method. +<p> + Smart Reply responses are generated by an entirely on-watch machine learning + model using the context provided by the MessagingStyle notification. No user + notification data is sent to Google servers to generate Smart Reply responses. </p> + <p>To enable Smart Reply for your notification action, you need to do the following: </p> <ol> - <li>Use <a href="{@docRoot}preview/features/notification-updates.html#style">{@code Notification.MessagingStyle}</a>. + <li>Use <a href="https://developer.android.com/reference/android/support/v4/app/NotificationCompat.MessagingStyle.html">{@code NotificationCompat.MessagingStyle}</a>. </li> - <li>Call the method {@code setAllowGeneratedReplies()} for the notification action. - For more information, see the downloadable - <a href="{@docRoot}preview/setup-sdk.html#docs-dl">API reference</a>. + <li>Call the method <a href="https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Action.Builder.html#setAllowGeneratedReplies(boolean)">{@code setAllowGeneratedReplies(true)}</a> + for the notification action. </li> <li>Ensure that the notification action has a <a href="{@docRoot}reference/android/app/RemoteInput.html">{@code RemoteInput}</a> @@ -236,3 +317,29 @@ Notification noti = new NotificationCompat.Builder() // 3) add an action with RemoteInput .extend(new WearableExtender().addAction(action)).build(); </pre> + +<h3 id="images">Adding images to a MessagingStyle notification</h3> +<p> + You can add images to a notification message by setting the appropriate MIME + type and placing the URI to the image in {@code NotificationCompat.MessagingStyle.Message.} + <a href="https://developer.android.com/reference/android/support/v4/app/NotificationCompat.MessagingStyle.Message.html#setData(java.lang.String, android.net.Uri)">{@code setData()}</a> method. +</p> +<p> + Here is the code snippet to set data of type image in a notification: +</p> +<pre> +NotificationCompat.MessagingStyle.Message message = new Message("sticker", 1, "Jeff") + .setData("image/png", stickerUri); + + NotificationCompat notification = new NotificationCompat.Builder() + .setStyle(new NotificationComapt.MessagingStyle("Me") + .addMessage(message) + .build()); + +</pre> +<p> + In the above code snippet the variable <code>stickerUri </code>is a Uri that + points to a publicly-accessible location, as described <a + href="https://developer.android.com/reference/android/support/v4/app/NotificationCompat.MessagingStyle.Message.html">here + </a>. +</p>
\ No newline at end of file diff --git a/docs/html/wear/preview/features/standalone-apps.jd b/docs/html/wear/preview/features/standalone-apps.jd new file mode 100644 index 000000000000..5c1930dedf86 --- /dev/null +++ b/docs/html/wear/preview/features/standalone-apps.jd @@ -0,0 +1,499 @@ +page.title=Standalone Apps +meta.keywords="wear-preview" +page.tags="wear-preview" +page.image=images/cards/card-n-sdk_2x.png + +@jd:body + +<div id="qv-wrapper"> +<div id="qv"> + +<h2>In this document</h2> + + <ul> + <li><a href="#planning_apps">Planning Your Phone and Watch Apps</a></li> + <li><a href="#network_access">Network Access and Cloud Messaging</a></li> + <li><a href="#background_services">Using Background Services</a></li> + <li><a href="#fcm">Cloud Notifications Using FCM</a></li> + <li><a href="#fcm-phone">Notifications from a Companion Phone</a></li> + </ul> + +</div> +</div> + + <p> + In Android Wear 2.0, apps can work independently of a phone. Users can + complete more tasks on a watch, without access to an Android or iOS + phone. + </p> + + <h2 id="planning_apps"> + Planning Your Phone and Watch Apps + </h2> + + <p> + A watch APK targeting Wear 2.0 should not be embedded in a phone APK. + For more information, see + <a href="{@docRoot}wear/preview/features/app-distribution.html"> + App Distribution</a>. + </p> + + <p> + Generally, the minimum and target API level for a standalone app, and + for Wear 2.0, is level 24. The minimum SDK level can be 23 + only if you are using the same APK + for Wear 1.0 and 2.0 (and thus have an embedded Wear 1.0 APK). + </p> + + <p> + If you build a standalone Wear 2.0 APK and will continue to have a + Wear 1.0 APK, please do both of the following: + </p> + + <ul> + <li>Provide a standalone version of the Wear APK, and + </li> + + <li>Continue embedding a version of the Wear APK in your phone APK + </li> + </ul> + + <p> + <strong>Caution</strong>: For the Wear 2.0 Developer Preview, if you + publish an update to your production phone APK that has removed an embedded + Wear APK, production users who update the phone APK before installing + your standalone Wear APK will lose their existing Wear app and its data. + Therefore, it's important that you continue to embed + your watch APK into your phone APK. + </p> + + <p> + <a href= + "https://developer.android.com/training/articles/wear-permissions.html"> + Run-time permissions</a> are required for standalone apps. + </p> + + <h3> + Shared code and data storage + </h3> + + <p> + Code can be shared between a Wear app and a phone app. Optionally, code + that is specific to a form factor can be in a separate module. + </p> + + <p> + For example, common code for networking can be in a shared library. + </p> + + <p> + You can use standard Android storage APIs to store data locally. + For example, you can use + the <a href= + "https://developer.android.com/reference/android/content/SharedPreferences.html"> + SharedPreferences APIs</a>, SQLite, or internal storage (as you would in + the case of a phone). + </p> + + <h3> + Detecting your phone app or watch app + </h3> + + <p> + If a user of your watch app needs your phone app, your watch app can + detect if the phone app is available. Using the <a href= + "https://developers.google.com/android/reference/com/google/android/gms/wearable/CapabilityApi"> + CapabilityApi</a>, your phone app or watch app can advertise its presence + to a paired device. It can do so statically and dynamically. When an app + is on a node in a user's Wear network (i.e., on a phone, paired watch, or + in the cloud), the <code>CapabilityApi</code> enables another + app to detect if it is installed. For more information, see <a href= + "https://developer.android.com/training/wearables/data-layer/messages.html#AdvertiseCapabilities"> + Advertise capabilities</a>. + </p> + + <p> + If your phone app is unavailable, your can check if the Play Store is + available on the phone, as described below, before directing the user to + go to the Play Store (to install your phone app). + </p> + + <h4> + Checking for Play Store availability on a phone + </h4> + + <p> + iPhones and some Android phones lack the Play Store. A standalone Wear + app can check if the paired phone has the Play Store, before directing a + user to go there to install your phone app. The + <code>PlayStoreAvailability</code> class contains a + <code>getPlayStoreAvailabilityOnPhone()</code> method that enables your + Wear app to check if a companion phone has the Play Store. + </p> + + <p> + More information about the <code>PlayStoreAvailability</code> class is + available when you <a href= + "https://developer.android.com/wear/preview/start.html#get_the_preview_reference_documentation"> + download the Android Wear 2.0 Preview Reference</a>. Here is a snippet + that uses the <code>getPlayStoreAvailabilityOnPhone()</code> method to + determine if the paired phone has the Play Store: + </p> + +<pre> +int playStoreAvailabilityOnPhone = +PlayStoreAvailability.getPlayStoreAvailabilityOnPhone(context); +</pre> + + <p> + The value returned by the <code>getPlayStoreAvailabilityOnPhone()</code> + method is one of the following: + </p> + + <table> + <tr> + <th> + <strong>Return value</strong> + </th> + <th> + <strong>Description</strong> + </th> + </tr> + + <tr> + <td> + <code>PLAY_STORE_ON_PHONE_AVAILABLE</code> + </td> + <td> + The Play Store is available on the companion phone. + </td> + </tr> + + <tr> + <td> + <code>PLAY_STORE_ON_PHONE_UNAVAILABLE</code> + </td> + <td> + The Play Store is not available on the companion phone. + </td> + </tr> + + <tr> + <td> + <code>PLAY_STORE_ON_PHONE_ERROR_UNKNOWN</code> + </td> + <td> + An error occurred in the check for the Play Store; another check + should be made later. + </td> + </tr> + </table> + + <h2 id="network_access"> + Network Access and Cloud Messaging + </h2> + + <p> + Android Wear apps can make their own network requests. When a watch has a + Bluetooth connection to a phone, the watch's network traffic is proxied + through the phone. When a phone is unavailable, Wi-Fi and cellular + networks are used, depending on the hardware. The Wear platform handles + transitions between networks. A watch's network access thus does not + require the <a href= + "https://developer.android.com/training/wearables/data-layer/index.html"> + Wearable Data Layer API</a>. + </p> + + <p> + For sending notifications, apps can directly use Firebase Cloud Messaging + (FCM), which replaces Google Cloud Messaging, or continue to use GCM. + </p> + + <p> + No APIs for network access or FCM are specific to Android Wear. + Refer to the existing documentation about <a href= + "https://developer.android.com/training/basics/network-ops/connecting.html"> + connecting to a network</a> and <a href= + "https://developers.google.com/cloud-messaging/">cloud messaging</a>. + </p> + + <p> + You can use protocols such as HTTP, TCP, and UDP. However, + the <a href="https://developer.android.com/reference/android/webkit/package-summary.html"> + android.webkit</a> APIs are not available. Therefore, + use of cookies is available by reading and writing headers on + requests and responses, but the <a href= + "https://developer.androidcom/reference/android/webkit/CookieManager.html"> + CookieManager</a> class is not available. + </p> + + <p> + FCM works well with + <a href="https://developer.android.com/training/monitoring-device-state/doze-standby.html"> + Doze</a>. + </p> + + <p> + Additionally, we recommend using the following: + </p> + + <ul> + <li>The <a href= + "https://developer.android.com/reference/android/app/job/JobScheduler.html"> + JobScheduler</a> API for asynchronous jobs, including polling at + regular intervals (described below) + </li> + + <li>Multi-networking APIs if you need to connect to specific network + types; see <a href= + "https://developer.android.com/about/versions/android-5.0.html#Wireless"> + Multiple Network Connections</a> + </li> + </ul> + + <p> + For foreground use cases, we currently recommend that you make a + request for an unmetered network. Here is an example of using + the multi-networking APIs to request an unmetered network: + </p> + +<pre> +ConnectivityManager.NetworkCallback networkCallback = + new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(Network network) { + // access network + } + }; +ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + +connectivityManager.requestNetwork(new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_NOT_METERED) + .build(), networkCallback); +</pre> + + <p> + We also recommend setting a timer for frontend scenarios + to prevent a user from potentially waiting for a long time. + When the network is no longer needed, or if the timer fires, + the network callback needs to be unregistered: + </p> + +<pre> +connectivityManager.unregisterNetworkCallback(networkCallback): +</pre> + + <p> + A Wear app can communicate with a phone app using the <a href= + "https://developer.android.com/training/wearables/data-layer/index.html">Wearable + Data Layer API</a>, but connecting to a network using that API is + discouraged. + </p> + + <h3 id="necessary_data"> + Obtaining only the necessary data + </h3> + + <p> + When obtaining data from the cloud, get only the necessary data. + Otherwise, you may introduce unnecessary latency, memory use, and battery + use. + </p> + + <p> + When a watch is connected over a Bluetooth LE connection, your app may + have access to a bandwidth of only 10 kilobytes per second. Therefore, + the following steps are recommended: + </p> + + <ul> + <li>Audit your network requests and responses for extra data that only is + for a phone app + </li> + + <li>Shrink large images before sending them over a network to a watch + </li> + </ul> + + <h2 id="background_services"> + Using Background Services + </h2> + + <p> + To ensure that background tasks are correctly executed, they must account + for <a href= + "https://developer.android.com/training/monitoring-device-state/doze-standby.html"> + Doze</a>. In Android 6.0, Doze and App Standby resulted in significant + improvements to battery life by allowing devices to enter deep sleep when + idle and stationary. + </p> + + <p> + Doze is <a href= + "https://developer.android.com/preview/behavior-changes.html#doze">enhanced</a> + in Android Nougat and Android Wear 2.0. When a screen turns off or enters + ambient mode for a long enough time, a subset of Doze can occur and + background tasks may be deferred for certain periods. Later, when a + device is stationary for an extended time, regular Doze occurs. + </p> + + <p> + You should schedule jobs with the <a href= + "https://developer.android.com/reference/android/app/job/JobScheduler.html"> + JobScheduler</a> API, which enables your app to register for Doze-safe + code execution. When scheduling jobs, you can select constraints such as + periodic execution and the need for connectivity or device charging. + It is important to configure jobs in a way that does not adversely + impact battery life. Jobs should use a + <a href="https://developer.android.com/reference/android/app/job/JobInfo.Builder.html"> + JobInfo.Builder</a> object to provide constraints and metadata, e.g. with + one or more of the following methods for a task: + </p> + + <ul> + <li>To schedule a task that requires networking, use + <code>setRequiredNetworkType(int networkType)</code>, specifying + <code>NETWORK_TYPE_ANY</code> or <code>NETWORK_TYPE_UNMETERED</code>; + note that <code>NETWORK_TYPE_UNMETERED</code> is for large data transfers + while <code>NETWORK_TYPE_ANY</code> is for small transfers + </li> + + <li>To schedule a task while charging, use + <code>setRequiresCharging(boolean requiresCharging)</code> + </li> + + <li>For specifying that a device is idle for a task, use + <code>setRequiresDeviceIdle(boolean requiresDeviceIdle)</code>; this + method can be useful for lower-priority background work or + synchronization, especially when used with + <code>setRequiresCharging</code> + </li> + </ul> + + <p> + Note that some low-bandwidth networks, such as Bluetooth LE, are + considered metered. + </p> + + <h3> + Scheduling with constraints + </h3> + + <p> + You can schedule a task that requires constraints. In the example below, + a <code>JobScheduler</code> object activates <code>MyJobService</code> + when the following constraints are met: + </p> + + <ul> + <li>Unmetered networking + </li> + + <li>Device charging + </li> + </ul> + + <p> + You can use the builder method <code>setExtras</code> to attach a bundle + of app-specific metadata to the job request. When your job executes, this + bundle is provided to your job service. Note the <code>MY_JOB_ID</code> + value passed to the <code>JobInfo.Builder</code> constructor. This + <code>MY_JOB_ID</code> value is an app-provided identifier. Subsequent + calls to cancel, and subsequent jobs created with that same value, will + update the existing job: + </p> + +<pre> +JobInfo jobInfo = new JobInfo.Builder(MY_JOB_ID, + new ComponentName(this, MyJobService.class)) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) + .setRequiresCharging(true) + .setExtras(extras) + .build(); +((JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE)) + .schedule(jobInfo); +</pre> + + <p> + Below is an implementation of <a href= + "https://developer.android.com/reference/android/app/job/JobService.html"> + JobService</a> to handle the job above. When the job executes, a + <code>JobParameters</code> object is passed into the + <code>onStartJob</code> method. The <code>JobParameters</code> object + enables you to get the job ID value along with any extras bundle provided + when scheduling the job. The <code>onStartJob</code> method is called on + the main application thread, and therefore any expensive logic should be + run from a separate thread. In the example, an <code>AsyncTask</code> is + used to run code in the background. When work is complete, you would call + the <code>jobFinished</code> method to notify <code>JobScheduler</code> + that the task is done: + </p> + +<pre> +public class MyJobService extends JobService { + @Override public boolean onStartJob(JobParameters params) { + new JobAsyncTask().execute(params); + return true; + } + + private class JobAsyncTask extends AsyncTask +</pre> + + <h2 id="fcm"> + Cloud Notifications Using FCM + </h2> + + <p> + FCM is the recommended way to send notifications to a watch. + </p> + + <p> + Provide for messages from FCM by collecting a registration token for a + device when your Wear app runs. Then include the token as part of the + destination when your server sends messages to the FCM REST endpoint. FCM + sends messages to the device identified by the token. + </p> + + <p> + An FCM message is in JSON format and can include one or both of the + following payloads: + </p> + + <ul> + <li> + <strong>Notification payload.</strong> When a notification payload is + received by a watch, the data is displayed to a user directly in the + notification stream. When the user taps the notification, your app is + launched. + </li> + + <li> + <strong>Data payload</strong>. The payload has a set of custom + key/value pairs. The payload and is delivered as data to your Wear app. + </li> + </ul> + + <p> + For more information and examples of payloads, see <a href= + "https://firebase.google.com/docs/cloud-messaging/concept-options">About + FCM Messages</a>. + </p> + + <h2 id="fcm-phone"> + Notifications from a Companion Phone + </h2> + + <p> + By default, notifications are bridged (shared) from a phone app to a + watch. If you have a standalone Wear app and a corresponding phone app, + duplicate notifications can occur. For example, the same notification + from FCM, received by both a phone and a watch, could be + displayed by both devices independently. + </p> + + <p> + For information about preventing duplicate notifications, see <a href= + "https://developer.android.com/wear/preview/features/bridger.html">Bridging + Mode for Notifications</a>. + </p> diff --git a/docs/html/wear/preview/features/wearable-recycler-view.jd b/docs/html/wear/preview/features/wearable-recycler-view.jd new file mode 100644 index 000000000000..f28a4722377e --- /dev/null +++ b/docs/html/wear/preview/features/wearable-recycler-view.jd @@ -0,0 +1,223 @@ + +page.title=Curved Layout +meta.tags="wear", "wear-preview", "RecyclerView" +page.tags="wear" + +@jd:body + + +<div id="qv-wrapper"> +<div id="qv"> + + <h2>In this document</h2> + <ol> + <li><a href="#creating">Creating a Curved Layout</a></li> + <li><a href="#adding">Adding a Circular Scrolling Gesture</a></li> + <li><a href="#aligning">Anchoring Children to the Curve</a></li> + </ol> + +</div> +</div> + + +<p> + Wear 2.0 introduces the {@code WearableRecyclerView} class for displaying + and manipulating a vertical list of items optimized for round displays. + {@code WearableRecyclerView} extends the existing + <a href="{@docRoot}reference/android/support/v7/widget/RecyclerView.html">{@code RecyclerView}</a> + class to provide a curved layout and a circular scrolling gesture in wearable apps. +</p> +<img src="https://android-dot-devsite.googleplex.com/wear/preview/images/wrv_new.png" + style="float:right;margin:10px 20px 0 0"> + +<p> + You can adapt to this interface in your wearable app by creating a new + {@code WearableRecyclerView} container. +</p> + +<p> + You should decide whether to use a {@code WearableRecyclerView}, based on + the kind of user experience you want to provide. We recommend using the + {@code WearableRecyclerView} for a simple, long list of items, such as an + application launcher, or a list contacts. Each item might have a short string + and an associated icon. Alternatively, each item might have only a string or + an icon. We do not recommend using a {@code WearableRecyclerView} for short + or complex lists. +</p> + +<p> + This document describes how to create a curved layout for your scrollable items + and properly align them along the curve. +</p> + + +<h2 id="creating">Creating a Curved Layout</h2> +<p>To create a curved layout for scrollable items in your wearable app: +</p> +<ul> + <li>Use {@code WearableRecyclerView} as your main container in the relevant + xml layout. + </li> + + <li>By default, {@code WearableRecyclerView} uses the {@code + DefaultOffsettingHelper} class to offset items in a curved layout on round + devices. If you wish to implement your own offsetting logic, you can extend the + abstract {@code WearableRecyclerView.OffsettingHelper} class and attach it to + the {@code WearableRecyclerView} using {@code + WearableRecyclerView.setOffsettingHelper} method. + + <pre> + CircularOffsettingHelper circularHelper = new CircularOffsettingHelper(); + mRecyclerView.setOffsettingHelper(circularHelper); + </pre> + + <pre> + public class CircularOffsettingHelper extends OffsettingHelper { + + @Override + public void updateChild(View child, WearableRecyclerView parent) { + int progress = child.getTop() / parent.getHeight(); + child.setTranslationX(-child.getHeight() * progress); + } + } + </pre> + + </li> + +</ul> + +<p class="note"> + <strong>Note:</strong> {@code DefaultOffsettingHelper} class + offsets the child items + along a predefined UX curve, but the operation can cut off part of the child + view if it is not scaled down accordingly. This is because the default curve + attempts to fit 5 items on the screen, regardless of their size. + If you do not wish to scale your items, you should consider additional padding. +</p> + +<h3>Examples</h3> +<p> + The following code example demonstrates how to add {@code WearableRecyclerView} + to a layout: +</p> +<pre> +<android.support.wearable.view.WearableRecyclerView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/recycler_launcher_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scrollbars="vertical" /> + </pre> + + +<p> + To customize the appearance of the children while scrolling (for example, + scale the icons and text while the items scroll away from the center), extend + the {@code DefaultOffsettingHelper} and override the {@code updateChild } + method. It is important to call the {@code super.updateChild(child, parent)} to + offset the children along the curve. However, if for any particular child you do + not wish them to follow a curve, you can chose not to call the super method for + that particular child. +</p> + +<pre> + +public class MyOffsettingHelper extends DefaultOffsettingHelper { + + /** How much should we scale the icon at most. */ + private static final float MAX_ICON_PROGRESS = 0.65f; + + private float mProgressToCenter; + + public OffsettingHelper() {} + + @Override + + public void updateChild(View child, WearableRecyclerView parent) { + super.updateChild(child, parent); + + + // Figure out % progress from top to bottom + float centerOffset = ((float) child.getHeight() / 2.0f) / (float) mParentView.getHeight(); + float yRelativeToCenterOffset = (child.getY() / mParentView.getHeight()) + centerOffset; + + // Normalize for center + mProgressToCenter = Math.abs(0.5f - yRelativeToCenterOffset); + // Adjust to the maximum scale + mProgressToCenter = Math.min(mProgressToCenter, MAX_ICON_PROGRESS); + + child.setScaleX(1 - mProgressToCenter); + child.setScaleY(1 - mProgressToCenter); + } +} + + +</pre> + + +<h2 id="adding">Adding a Circular Scrolling Gesture</h2> + +<p> + By default, circular scrolling is disabled in the {@code + WearableRecyclerView}. If you want to enable circular scrolling gesture + in your child view, use the {@code WearavleRecyclerView}’s {@code + setCircularScrollingGestureEnabled()} method. You can also customize the + circular scrolling gesture by defining one or both of the following: +</p> + +<ul> + <li>How many degrees the user has to rotate by to scroll through one screen height. + This effectively influences the speed of the scolling - + {@code setScrollDegreesPerScreen} - the default value is set at 180 degrees. + </li> + + <li> + The width of a virtual ‘bezel’ near the the edge of the screen in which the + gesture will be recognized - {@code setBezelWidth} - the default value is set + at 1. This is expressed as a fraction of the radius of the view. +</ul> + + +<p>The following code snippet shows how to set these methods:</p> + +<pre> + setCircularScrollingGestureEnabled(true); + setBezelWidth(0.5f); + setScrollDegreesPerScreen(90); +</pre> + +<h2 id="aligning"> Anchoring Children to the Curve </h2> + +<p> + To ensure that the layout for WearableRecyclerView is adaptable to different + types of child views, the WearableRecyclerView class, by default, chooses the + middle left edge (X=0, Y=Half the child height) as the anchor coordinates for + the child item. Using the default anchor coordinates can result in offsetting + the child items from the left edge of the watch face. To customize the anchor + coordinates of your child view along the curve, you can overwrite the + {@code adjustAnchorOffsetXY()} method. You can calculate the X (horizontal) + and Y (vertical) offset of the child item, and set it using the + {@code adjustAnchorOffsetXY()} method to properly align items + along the curve. The coordinates should be with relation to the child view. +</p> + +<p><img src="{@docRoot}wear/preview/images/alignment.png"/></p> +<p><b>Figure 1</b>. Imaginary UX curve and anchor points on the curve.</p> + +<p> + The code snippet below, calculates the X offset for a child item in which the + width of the icon is same as the height of the child item. In this case, the + anchor coordinates for the child item are at the center of the icon. + +</p> +<img src="{@docRoot}wear/preview/images/center_align.png" style="float:left;margin:10px 20px 0 0"/> + +<pre> + @Override + protected void adjustAnchorOffsetXY(View child, float[] anchorOffsetXY) { + anchorOffsetXY[0] = child.getHeight() / 2.0f; + } +</pre> + + diff --git a/docs/html/wear/preview/images/alignment.png b/docs/html/wear/preview/images/alignment.png Binary files differnew file mode 100644 index 000000000000..525b3341789a --- /dev/null +++ b/docs/html/wear/preview/images/alignment.png diff --git a/docs/html/wear/preview/images/apk-details.png b/docs/html/wear/preview/images/apk-details.png Binary files differnew file mode 100644 index 000000000000..eb3b8591f53f --- /dev/null +++ b/docs/html/wear/preview/images/apk-details.png diff --git a/docs/html/wear/preview/images/apk-tabs.png b/docs/html/wear/preview/images/apk-tabs.png Binary files differnew file mode 100644 index 000000000000..949b98f75b7d --- /dev/null +++ b/docs/html/wear/preview/images/apk-tabs.png diff --git a/docs/html/wear/preview/images/center_align.png b/docs/html/wear/preview/images/center_align.png Binary files differnew file mode 100644 index 000000000000..ca88ad77bac4 --- /dev/null +++ b/docs/html/wear/preview/images/center_align.png diff --git a/docs/html/wear/preview/images/current-apk.png b/docs/html/wear/preview/images/current-apk.png Binary files differnew file mode 100644 index 000000000000..2545f925f608 --- /dev/null +++ b/docs/html/wear/preview/images/current-apk.png diff --git a/docs/html/wear/preview/images/inline_action.png b/docs/html/wear/preview/images/inline_action.png Binary files differnew file mode 100644 index 000000000000..7ecaafeb2544 --- /dev/null +++ b/docs/html/wear/preview/images/inline_action.png diff --git a/docs/html/wear/preview/images/wrv_new.png b/docs/html/wear/preview/images/wrv_new.png Binary files differnew file mode 100644 index 000000000000..c413c59a77c3 --- /dev/null +++ b/docs/html/wear/preview/images/wrv_new.png diff --git a/docs/html/wear/preview/program.jd b/docs/html/wear/preview/program.jd index e2bf92f80c6c..4f2fb5cecfb1 100644 --- a/docs/html/wear/preview/program.jd +++ b/docs/html/wear/preview/program.jd @@ -143,8 +143,9 @@ page.image=images/cards/card-n-sdk_2x.png <p> At milestone 4, you'll have access to the final Android Wear 2.0 APIs and SDK to develop with, as well as near-final system images to test - system behaviors and features. Android Wear 2.0 will use the Android N - API level at this time. You can begin final compatibility testing of your + system behaviors and features. Android Wear 2.0 will use the + Android 7.0 API level at this time. + You can begin final compatibility testing of your legacy apps and refine any new code that is using the Android Wear 2.0 APIs or features. </p> diff --git a/docs/html/wear/preview/start.jd b/docs/html/wear/preview/start.jd index 8fccdc82cd5a..c9720dceb842 100644 --- a/docs/html/wear/preview/start.jd +++ b/docs/html/wear/preview/start.jd @@ -29,7 +29,7 @@ page.image=images/cards/card-n-sdk_2x.png <p> If you want an environment for basic compatibility - testing of your app, you can use your current APK and a + testing, you can use your current APK and a supported watch or an emulator. You don't necessarily need to update your full development environment to do basic testing. To simply test your app's compatibility with a preview system image, see <a href= @@ -48,10 +48,8 @@ page.image=images/cards/card-n-sdk_2x.png </h2> <p> - 1. For compatibility with the <a href="{@docRoot}preview/overview.html">N - Developer Preview</a>, follow the <a href= - "{@docRoot}preview/setup-sdk.html">setup instructions</a> for installing - the latest version of Android Studio. + 1. For compatibility with Android 7.0, install the latest version of + <a href="https://developer.android.com/studio/index.html">Android Studio</a>. </p> <p> @@ -63,7 +61,7 @@ page.image=images/cards/card-n-sdk_2x.png <ul> <li>Under the <strong>SDK Platforms tab</strong>: <ul> - <li>Android N Preview + <li>Android 7.0 (Nougat) </li> </ul> </li> @@ -107,10 +105,10 @@ page.image=images/cards/card-n-sdk_2x.png <tr> <td> - <a href="http://storage.googleapis.com/androiddevelopers/shareables/wear-preview/wearable-support-preview-2-docs.zip">wearable-support-preview-2-docs.zip</a> + <a href="http://storage.googleapis.com/androiddevelopers/shareables/wear-preview/wearable-support-preview-3-docs.zip">wearable-support-preview-3-docs.zip</a> </td> - <td>MD5: afb770c9c5c0431bbcbdde186f1eae06<br> - SHA-1: 81d681e61cee01f222ea82e83297d23c4e55b8f3 + <td>MD5: 22bae00e473e39e320aae8ea09a001a5<br> + SHA-1: 474502cc7092bcf0bd671441b8654aa8d6c155ed </td> </tr> </table> @@ -163,7 +161,7 @@ page.image=images/cards/card-n-sdk_2x.png following, which requires that your the Google Repository <a href= "#install_android_studio_and_the_latest_packages">is the latest version</a>: - <code>compile 'com.google.android.support:wearable:2.0.0-alpha2'</code> + <code>compile 'com.google.android.support:wearable:2.0.0-alpha3'</code> </li> </ul> </li> @@ -190,12 +188,12 @@ page.image=images/cards/card-n-sdk_2x.png </li> <li>Optionally, select the <strong>Phone and Tablet</strong> option. If - you plan to use N Preview APIs in a phone app, then the Minimum SDK - option list, select <strong>API N: Android 6.x (N Preview)</strong>. + you plan to use Android 7.0 APIs in a phone app, then the Minimum SDK + option list, select <strong>API 24: Android 7.0 (Nougat)</strong>. </li> <li>Select the <strong>Wear</strong> option, and in the Minimum SDK - option list, select the latest available (<strong>N Preview</strong>) + option list, select the latest available (<strong>API Nougat</strong>) option. Click <strong>Next</strong> until you exit the Create New Project wizard. </li> @@ -215,7 +213,7 @@ page.image=images/cards/card-n-sdk_2x.png following, which requires that your the Google Repository <a href= "#install_android_studio_and_the_latest_packages">is the latest version</a>: - <code>compile 'com.google.android.support:wearable:2.0.0-alpha2'</code> + <code>compile 'com.google.android.support:wearable:2.0.0-alpha3'</code> </li> </ul> </li> diff --git a/docs/html/wear/preview/support.jd b/docs/html/wear/preview/support.jd index 78b4e4b854ac..6006627a4e16 100644 --- a/docs/html/wear/preview/support.jd +++ b/docs/html/wear/preview/support.jd @@ -23,7 +23,9 @@ page.tags="preview", "developer preview" <ul> <li><a href="#general">General Advisories</a></li> + <li><a href="#platform-version">Platform API Version</a></li> <li><a href="#deprecations">Deprecations</a></li> + <li><a href="#dp3">Developer Preview 3</a></li> <li><a href="#dp2">Developer Preview 2</a></li> <li><a href="#dp1">Developer Preview 1</a></li> </ul> @@ -46,10 +48,25 @@ page.tags="preview", "developer preview" panics and crashes. </li> <li>Some apps <strong>may not function as expected</strong> on the new - platform version. This includes Google’s apps and other apps. + platform version. This includes Google's apps and other apps. </li> </ul> +<h2 id="platform-version"> + Platform API Version +</h2> + +<p> + The Android Platform API version is incremented to 24 to match Android 7.0. + You can update the following in your Android Wear 2.0 Preview project + to <strong>24</strong>: +</p> + +<ul> + <li><code>compileSdkVersion</code></li> + <li><code>targetSdkVersion</code></li> +</ul> + <h2 id="deprecations">Deprecations</h2> <p>The following fields are deprecated in the preview:</p> @@ -64,6 +81,291 @@ page.tags="preview", "developer preview" </li> </ul> +<h2 id="dp3">Developer Preview 3</h2> + +<div class="wrap"> + <div class="cols"> + <div class="col-6of12"> + <p><em>Date: September 2016<br /> + Builds: Wearable Support 2.0.0-alpha3, NVE68J<br/> + Emulator support: x86 & ARM (32-bit)<br/> + </em></p> + </div> + </div> +</div> + +<h3 id="new-in-fdp3"> + New in Preview 3 +</h3> + + <p> + For access to system images and the companion app for Preview 3, see + <a href="https://developer.android.com/wear/preview/downloads.html"> + Download and Test with a Device</a>. + </p> + + <h4> + Additions for standalone apps and the Play Store on Wear + </h4> + + <p> + For information about planning your Wear 2.0 app, see <a href= + "https://developer.android.com/wear/preview/features/standalone-apps.html"> + Standalone Apps</a>. + </p> + + <p> + Generally, the minimum and target SDK level for Wear 2.0, and for a + standalone APK, is level 24. The minimum SDK level can be 23 + only if you are using the same APK + for Wear 1.0 and 2.0 (and thus have an embedded Wear 1.0 APK). + </p> + + <p> + Run-time permissions are required. + </p> + + <p> + For information about distributing your Wear 2.0 app, see <a href= + "https://developer.android.com/wear/preview/features/app-distribution.html"> + App Distribution</a>. + </p> + + <h4 id="additions-to-the-complications-api"> + Complications API additions + </h4> + + <p> + For Preview 3, additions and changes have been made to the Complications + API. The <a href= + "https://developer.android.com/wear/preview/features/complications.html">documentation</a> + includes information about the following additions and changes: + </p> + + <ul> + <li>To receive complication data and open the provider chooser, a watch + face must have the <code>RECEIVE_COMPLICATION_DATA</code> permission. + </li> + + <li>To ease a request for the new permission and the starting of the + chooser, the <code>ComplicationHelperActivity</code> class is available + in the wearable support library. This class should be used instead of + <code>ProviderChooserIntent</code> to start the chooser in almost all + cases. + </li> + + <li>Watch faces can specify default providers that are used until a user + selects a provider. + </li> + + <li>The complication types used for "empty" data are changed. + </li> + + <li>A new permission was added to ensure that only the Android Wear + system can bind to provider services. + </li> + </ul> + + <p> + For changes related to the <code>ComplicationData</code> object, see + <a href= + "https://developer.android.com/wear/preview/behavior-changes.html">Behavior + Changes</a>. + </p> + + <h4 id="wearable-recycler-view-api"> + Curved Layout + </h4> + + <p> + For information about creating a curved layout using + the <code>WearableRecyclerView</code> API in your Wear 2.0 app, see + <a href="https://developer.android.com/wear/preview/features/wearable-recycler-view.html"> + Curved Layout</a>. + </p> + + <h4 id="notifications-features-fdp3"> + Notifications features + </h4> + + <p> + To learn about adding an inline action to a notification, + see <a href="https://developer.android.com/wear/preview/notifications.html#inline">Inline + Action</a>. + </p> + + <p> + To learn about adding images to a notification, see + <a href= + "https://developer.android.com/wear/preview/notifications.html#images">Adding + images to a notification</a>. + </p> + + <p> + For additions related to the bridging of notifications from a companion + app to a watch, see <a href= + "https://developer.android.com/wear/preview/features/bridger.html">Bridging + Mode for Notifications</a>. + </p> + + <h4 id="additions-for-smart-reply"> + Smart Reply additions + </h4> + + <p> + Smart Reply responses are generated by an entirely on-watch, + machine-learning model using the context provided by <a href= + "https://developer.android.com/wear/preview/features/notifications.html#messaging"> + MessagingStyle</a> notifications. Use the <a href= + "https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Action.Builder.html#setAllowGeneratedReplies(boolean)"> + setAllowGeneratedReplies(boolean)</a> method to enable Smart Reply for + your <code>MessagingStyle</code> notification. + </p> + + <h3 id="known-issues-3"> + Known Issues + </h3> + + <h4 id="notifications"> + Notifications + </h4> + + <ul> + <li>The <code>MessagingStyle</code> <a href= + "https://developer.android.com/wear/preview/features/notifications.html#images"> + notifications with images</a> posted by standalone apps don't show + images in the notification (i.e., bridged notifications show images, + but standalone notifications don't). + </li> + + <li>This preview release does not include support for notification + groups. + </li> + + <li>With Wear 2.0, a watch can receive notifications directly from + Firebase Cloud Messaging (FCM), which replaces Google Cloud Messaging + (GCM). However, in Preview 3 of Wear 2.0, FCM does not function with + iOS-paired watches. + </li> + + <li>Smart Reply responses are only shown in <code>RemoteInput</code> when + <code>RemoteInput</code> is called from a <code>MessagingStyle</code> + expanded notification. Smart Reply responses are not shown in + <code>RemoteInput</code> when <code>RemoteInput</code> is called from an + <a href= + "https://developer.android.com/wear/preview/features/notifications.html#inline"> + inline action</a> within the stream—an action set with the <a href= + "https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Action.WearableExtender.html#setHintDisplayActionInline(boolean)"> + setHintDisplayActionInline(true)</a> method. + </li> + </ul> + + <h4 id="companion-app"> + Companion app + </h4> + + <ul> + <li>The preview companion app is not compatible with Android 4.3 + (Jelly Bean MR2), which has an SDK build version code of: + <code>JELLY_BEAN_MR2</code></li> + </ul> + + <ul> + <li>In permission screens in the preview companion app: + If you deny a permission, you cannot + proceed. Instead of denying a permission, tap <strong>Skip</strong>. + </li> + </ul> + + + <h4 id="developer-console"> + Developer Console + </h4> + + <ul> + <li>If you set a minimum SDK version of 24, the Play Developer Console + states that there are few supported devices. + </li> + </ul> + + <h4 id="system-user-interface"> + System user interface and apps + </h4> + + <ul> + <li>Dismissing multiple notifications can cause an app to forcibly close. + </li> + + <li>The "Ok Google" detection and voice transcription may not work + reliably. + </li> + + <li>Google Fit is not available with Preview 3. + </li> + + <li>Syncing for embedded apps is not enabled for the preview. Therefore, + to test an app on a device, add it to the Play Store or side-load it + onto a watch. Some existing Wear apps, e.g. Google Maps, are only + using the embedded apps mechanism currently, and are therefore not + installable on the preview (and therefore do not appear on the watch). + </li> + + <li>In Play Store search results on the watch, + results other than apps sometimes appear. + </li> + + <li>Media controls/notifications are not bridged + to the watch from an Android KitKat phone. + </li> + </ul> + + <h4 id="account"> + Account sync + </h4> + + <ul> + <li>Account sync initiated from watch settings may not work reliably. + Instead, add accounts from the setup flow of the Android Wear app, or using + the Accounts settings for a device from the Android Wear app. + </li> + + <li>The list of accounts that can be synced is the same as the list of accounts + on the phone. So to add a new account, use the Android settings on the phone, + and then proceed to Android Wear app to sync that account. + </li> + </ul> + + <h4 id="devices"> + Devices + </h4> + + <ul> + <li>In Android Wear emulators, the Play Store app requires that an + account is synced to the device before the app can be opened. + </li> + + <li>On the Huawei Watch, selecting the language, followed by multiple + acknowledgement dialogues, results in a black screen. + </li> + + <li>On the LG Watch Urbane 2nd Edition, when answering a call from the + watch, the watch does not provide audio from the caller. + </li> + </ul> + + <h4 id="smart-reply"> + Smart Reply + </h4> + + <ul> + <li>Smart Reply is only available if your watch's system language is + English. + </li> + + <li>Smart Reply responses are not generated for all messages. + </li> + </ul> + <h2 id="dp2">Developer Preview 2</h2> <div class="wrap"> @@ -78,24 +380,9 @@ page.tags="preview", "developer preview" </div> <h3 id="new-in-fdp2"> - <strong>New in Preview 2</strong> + New in Preview 2 </h3> -<h4 id="platform-version-24"> - Platform API Version -</h4> - -<p> - The Android Platform API version is incremented to 24 to match Android Nougat. - You can update the following in your Android Wear 2.0 Preview project - to <strong>24</strong>: -</p> - -<ul> - <li><code>compileSdkVersion</code></li> - <li><code>targetSdkVersion</code></li> -</ul> - <h4 id="wearable-drawers"> Wearable drawers </h4> @@ -174,7 +461,7 @@ page.tags="preview", "developer preview" </p> <h3 id="known-issues-2"> - <strong>Known Issues</strong> + Known Issues </h3> <h4 id="notifications-2"> @@ -239,6 +526,10 @@ page.tags="preview", "developer preview" <li>Unable to turn off the Wi-Fi on a wearable. </li> + + <li>After music is played on a companion phone, + music card notifications are not mirrored to the watch. + </li> </ul> <h4 id="companion-app-2"> diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index 49721cf42666..7ce750d6e000 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -48,11 +48,6 @@ public final class Bitmap implements Parcelable { // pixel data. private static final long NATIVE_ALLOCATION_SIZE = 32; - /** - * Backing buffer for the Bitmap. - */ - private byte[] mBuffer; - // Convenience for JNI access private final long mNativePtr; @@ -108,7 +103,7 @@ public final class Bitmap implements Parcelable { * int (pointer). */ // called from JNI - Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density, + Bitmap(long nativeBitmap, int width, int height, int density, boolean isMutable, boolean requestPremultiplied, byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) { if (nativeBitmap == 0) { @@ -119,7 +114,6 @@ public final class Bitmap implements Parcelable { mHeight = height; mIsMutable = isMutable; mRequestPremultiplied = requestPremultiplied; - mBuffer = buffer; mNinePatchChunk = ninePatchChunk; mNinePatchInsets = ninePatchInsets; @@ -128,10 +122,7 @@ public final class Bitmap implements Parcelable { } mNativePtr = nativeBitmap; - long nativeSize = NATIVE_ALLOCATION_SIZE; - if (buffer == null) { - nativeSize += getByteCount(); - } + long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount(); NativeAllocationRegistry registry = new NativeAllocationRegistry( Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize); registry.registerNativeAllocation(this, nativeBitmap); @@ -256,12 +247,8 @@ public final class Bitmap implements Parcelable { if (!isMutable()) { throw new IllegalStateException("only mutable bitmaps may be reconfigured"); } - if (mBuffer == null) { - throw new IllegalStateException("native-backed bitmaps may not be reconfigured"); - } - nativeReconfigure(mNativePtr, width, height, config.nativeInt, - mBuffer.length, mRequestPremultiplied); + nativeReconfigure(mNativePtr, width, height, config.nativeInt, mRequestPremultiplied); mWidth = width; mHeight = height; } @@ -343,7 +330,6 @@ public final class Bitmap implements Parcelable { // false indicates that it is still in use at the native level and these // objects should not be collected now. They will be collected later when the // Bitmap itself is collected. - mBuffer = null; mNinePatchChunk = null; } mRecycled = true; @@ -1273,12 +1259,7 @@ public final class Bitmap implements Parcelable { * @see #reconfigure(int, int, Config) */ public final int getAllocationByteCount() { - if (mBuffer == null) { - // native backed bitmaps don't support reconfiguration, - // so alloc size is always content size - return getByteCount(); - } - return mBuffer.length; + return nativeGetAllocationByteCount(mNativePtr); } /** @@ -1695,8 +1676,7 @@ public final class Bitmap implements Parcelable { private static native long nativeGetNativeFinalizer(); private static native boolean nativeRecycle(long nativeBitmap); private static native void nativeReconfigure(long nativeBitmap, int width, int height, - int config, int allocSize, - boolean isPremultiplied); + int config, boolean isPremultiplied); private static native boolean nativeCompress(long nativeBitmap, int format, int quality, OutputStream stream, @@ -1742,4 +1722,5 @@ public final class Bitmap implements Parcelable { private static native boolean nativeSameAs(long nativeBitmap0, long nativeBitmap1); private static native long nativeRefPixelRef(long nativeBitmap); private static native void nativePrepareToDraw(long nativeBitmap); + private static native int nativeGetAllocationByteCount(long nativeBitmap); } diff --git a/graphics/java/android/graphics/drawable/Drawable.java b/graphics/java/android/graphics/drawable/Drawable.java index af20f8a7d56f..10f0dda5a976 100644 --- a/graphics/java/android/graphics/drawable/Drawable.java +++ b/graphics/java/android/graphics/drawable/Drawable.java @@ -109,6 +109,9 @@ import java.util.Collection; * <li> <b>Nine Patch</b>: an extension to the PNG format allows it to * specify information about how to stretch it and place things inside of * it. + * <li><b>Vector</b>: a drawable defined in an XML file as a set of points, + * lines, and curves along with its associated color information. This type + * of drawable can be scaled without loss of display quality. * <li> <b>Shape</b>: contains simple drawing commands instead of a raw * bitmap, allowing it to resize better in some cases. * <li> <b>Layers</b>: a compound drawable, which draws multiple underlying diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index 44d701057c04..eedc9e7d5333 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -148,7 +148,7 @@ enum DebugLevel { * This will disable the use of EGL_EXT_buffer_age and BUFFER_PRESERVED. * Default is "true" */ -#define PROPERTY_ENABLE_PARTIAL_UPDATES "debug.hwui.enable_partial_updates" +#define PROPERTY_ENABLE_PARTIAL_UPDATES "debug.hwui.use_partial_updates" #define PROPERTY_FILTER_TEST_OVERHEAD "debug.hwui.filter_test_overhead" diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp index ac6a28fe6289..86731c9581be 100644 --- a/libs/hwui/renderthread/EglManager.cpp +++ b/libs/hwui/renderthread/EglManager.cpp @@ -134,7 +134,12 @@ void EglManager::initialize() { void EglManager::initExtensions() { auto extensions = StringUtils::split( eglQueryString(mEglDisplay, EGL_EXTENSIONS)); - EglExtensions.bufferAge = extensions.has("EGL_EXT_buffer_age"); + // For our purposes we don't care if EGL_BUFFER_AGE is a result of + // EGL_EXT_buffer_age or EGL_KHR_partial_update as our usage is covered + // under EGL_KHR_partial_update and we don't need the expanded scope + // that EGL_EXT_buffer_age provides. + EglExtensions.bufferAge = extensions.has("EGL_EXT_buffer_age") + || extensions.has("EGL_KHR_partial_update"); EglExtensions.setDamage = extensions.has("EGL_KHR_partial_update"); LOG_ALWAYS_FATAL_IF(!extensions.has("EGL_KHR_swap_buffers_with_damage"), "Missing required extension EGL_KHR_swap_buffers_with_damage"); diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java index 96ea8349627f..06dd3db9ab4f 100644 --- a/media/java/android/media/MediaRouter.java +++ b/media/java/android/media/MediaRouter.java @@ -219,12 +219,25 @@ public class MediaRouter { } if (mBluetoothA2dpRoute != null) { - if (mSelectedRoute == mDefaultAudioVideo || mSelectedRoute == null) { + final boolean a2dpEnabled = isBluetoothA2dpOn(); + if (mSelectedRoute == mBluetoothA2dpRoute && !a2dpEnabled) { + selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo, false); + } else if ((mSelectedRoute == mDefaultAudioVideo || mSelectedRoute == null) && + a2dpEnabled) { selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute, false); } } } + boolean isBluetoothA2dpOn() { + try { + return mAudioService.isBluetoothA2dpOn(); + } catch (RemoteException e) { + Log.e(TAG, "Error querying Bluetooth A2DP state", e); + return false; + } + } + void updateDiscoveryRequest() { // What are we looking for today? int routeTypes = 0; @@ -893,11 +906,6 @@ public class MediaRouter { static void selectRouteStatic(int types, @NonNull RouteInfo route, boolean explicit) { Log.v(TAG, "Selecting route: " + route); assert(route != null); - if (route == sStatic.mDefaultAudioVideo && sStatic.mBluetoothA2dpRoute != null) { - Log.i(TAG, "Change the route to a BT route: " + sStatic.mBluetoothA2dpRoute - + "\nDo not select the default route when a BT route is available."); - route = sStatic.mBluetoothA2dpRoute; - } final RouteInfo oldRoute = sStatic.mSelectedRoute; if (oldRoute == route) return; if (!route.matchesTypes(types)) { @@ -907,6 +915,16 @@ public class MediaRouter { return; } + final RouteInfo btRoute = sStatic.mBluetoothA2dpRoute; + if (btRoute != null && (types & ROUTE_TYPE_LIVE_AUDIO) != 0 && + (route == btRoute || route == sStatic.mDefaultAudioVideo)) { + try { + sStatic.mAudioService.setBluetoothA2dpOn(route == btRoute); + } catch (RemoteException e) { + Log.e(TAG, "Error changing Bluetooth A2DP state", e); + } + } + final WifiDisplay activeDisplay = sStatic.mDisplayService.getWifiDisplayStatus().getActiveDisplay(); final boolean oldRouteHasAddress = oldRoute != null && oldRoute.mDeviceAddress != null; @@ -946,7 +964,7 @@ public class MediaRouter { static void selectDefaultRouteStatic() { // TODO: Be smarter about the route types here; this selects for all valid. if (sStatic.mSelectedRoute != sStatic.mBluetoothA2dpRoute - && sStatic.mBluetoothA2dpRoute != null) { + && sStatic.mBluetoothA2dpRoute != null && sStatic.isBluetoothA2dpOn()) { selectRouteStatic(ROUTE_TYPE_ANY, sStatic.mBluetoothA2dpRoute, false); } else { selectRouteStatic(ROUTE_TYPE_ANY, sStatic.mDefaultAudioVideo, false); @@ -1278,7 +1296,12 @@ public class MediaRouter { selectedRoute == sStatic.mDefaultAudioVideo) { dispatchRouteVolumeChanged(selectedRoute); } else if (sStatic.mBluetoothA2dpRoute != null) { - dispatchRouteVolumeChanged(sStatic.mBluetoothA2dpRoute); + try { + dispatchRouteVolumeChanged(sStatic.mAudioService.isBluetoothA2dpOn() ? + sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudioVideo); + } catch (RemoteException e) { + Log.e(TAG, "Error checking Bluetooth A2DP state to report volume change", e); + } } else { dispatchRouteVolumeChanged(sStatic.mDefaultAudioVideo); } diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java index af1410bf7965..3cb01de7513d 100644 --- a/media/java/android/media/RingtoneManager.java +++ b/media/java/android/media/RingtoneManager.java @@ -727,7 +727,9 @@ public class RingtoneManager { String setting = getSettingForType(type); if (setting == null) return; - ringtoneUri = ContentProvider.maybeAddUserId(ringtoneUri, context.getUserId()); + if(!isInternalRingtoneUri(ringtoneUri)) { + ringtoneUri = ContentProvider.maybeAddUserId(ringtoneUri, context.getUserId()); + } Settings.System.putStringForUser(resolver, setting, ringtoneUri != null ? ringtoneUri.toString() : null, context.getUserId()); @@ -744,6 +746,12 @@ public class RingtoneManager { } } + private static boolean isInternalRingtoneUri(Uri uri) { + Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(uri); + return uriWithoutUserId == null ? false : uriWithoutUserId.toString() + .startsWith(MediaStore.Audio.Media.INTERNAL_CONTENT_URI.toString()); + } + /** * Try opening the given ringtone locally first, but failover to * {@link IRingtonePlayer} if we can't access it directly. Typically happens diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp index 810996ec79a7..c2c66fdb6ca9 100644 --- a/media/jni/android_media_MediaCodec.cpp +++ b/media/jni/android_media_MediaCodec.cpp @@ -32,6 +32,7 @@ #include <gui/Surface.h> #include <media/ICrypto.h> +#include <media/MediaCodecBuffer.h> #include <media/stagefright/MediaCodec.h> #include <media/stagefright/foundation/ABuffer.h> #include <media/stagefright/foundation/ADebug.h> @@ -407,7 +408,7 @@ status_t JMediaCodec::getOutputFormat(JNIEnv *env, size_t index, jobject *format status_t JMediaCodec::getBuffers( JNIEnv *env, bool input, jobjectArray *bufArray) const { - Vector<sp<ABuffer> > buffers; + Vector<sp<MediaCodecBuffer> > buffers; status_t err = input @@ -425,7 +426,7 @@ status_t JMediaCodec::getBuffers( } for (size_t i = 0; i < buffers.size(); ++i) { - const sp<ABuffer> &buffer = buffers.itemAt(i); + const sp<MediaCodecBuffer> &buffer = buffers.itemAt(i); jobject byteBuffer = NULL; err = createByteBufferFromABuffer( @@ -446,8 +447,9 @@ status_t JMediaCodec::getBuffers( } // static +template <typename T> status_t JMediaCodec::createByteBufferFromABuffer( - JNIEnv *env, bool readOnly, bool clearBuffer, const sp<ABuffer> &buffer, + JNIEnv *env, bool readOnly, bool clearBuffer, const sp<T> &buffer, jobject *buf) const { // if this is an ABuffer that doesn't actually hold any accessible memory, // use a null ByteBuffer @@ -492,7 +494,7 @@ status_t JMediaCodec::createByteBufferFromABuffer( status_t JMediaCodec::getBuffer( JNIEnv *env, bool input, size_t index, jobject *buf) const { - sp<ABuffer> buffer; + sp<MediaCodecBuffer> buffer; status_t err = input @@ -509,7 +511,7 @@ status_t JMediaCodec::getBuffer( status_t JMediaCodec::getImage( JNIEnv *env, bool input, size_t index, jobject *buf) const { - sp<ABuffer> buffer; + sp<MediaCodecBuffer> buffer; status_t err = input diff --git a/media/jni/android_media_MediaCodec.h b/media/jni/android_media_MediaCodec.h index c0c47ef2aeda..88b566a62dff 100644 --- a/media/jni/android_media_MediaCodec.h +++ b/media/jni/android_media_MediaCodec.h @@ -146,8 +146,9 @@ private: status_t mInitStatus; + template <typename T> status_t createByteBufferFromABuffer( - JNIEnv *env, bool readOnly, bool clearBuffer, const sp<ABuffer> &buffer, + JNIEnv *env, bool readOnly, bool clearBuffer, const sp<T> &buffer, jobject *buf) const; void cacheJavaObjects(JNIEnv *env); diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java index f688a8e11f6b..b3cfea520767 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java @@ -760,6 +760,10 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat mPrintJob.setPrinterId(printerInfo.getId()); mPrintJob.setPrinterName(printerInfo.getName()); + if (printerInfo.getCapabilities() != null) { + updatePrintAttributesFromCapabilities(printerInfo.getCapabilities()); + } + mDestinationSpinnerAdapter.ensurePrinterInVisibleAdapterPosition(printerInfo); MetricsLogger.action(this, MetricsEvent.ACTION_PRINTER_SELECT_ALL, diff --git a/packages/SettingsLib/res/values-ja/strings.xml b/packages/SettingsLib/res/values-ja/strings.xml index 812f53d28456..f657c1e21687 100644 --- a/packages/SettingsLib/res/values-ja/strings.xml +++ b/packages/SettingsLib/res/values-ja/strings.xml @@ -28,10 +28,8 @@ <string name="wifi_disabled_wifi_failure" msgid="3081668066612876581">"WiFi接続エラー"</string> <string name="wifi_disabled_password_failure" msgid="8659805351763133575">"認証に問題"</string> <string name="wifi_not_in_range" msgid="1136191511238508967">"圏外"</string> - <!-- no translation found for wifi_no_internet_no_reconnect (2211781637653149657) --> - <skip /> - <!-- no translation found for wifi_no_internet (5011955173375805204) --> - <skip /> + <string name="wifi_no_internet_no_reconnect" msgid="2211781637653149657">"インターネット アクセスを検出できないため、自動的に再接続されません。"</string> + <string name="wifi_no_internet" msgid="5011955173375805204">"インターネットに接続していません。"</string> <string name="saved_network" msgid="4352716707126620811">"<xliff:g id="NAME">%1$s</xliff:g>で保存"</string> <string name="connected_via_wfa" msgid="3805736726317410714">"Wi‑Fiアシスタント経由で接続"</string> <string name="connected_via_passpoint" msgid="2826205693803088747">"%1$s経由で接続"</string> diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 7d9cc538cfa4..1a1aa57a0148 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -65,9 +65,9 @@ <!-- Summary for the remembered network but currently not in range. --> <string name="wifi_not_in_range">Not in range</string> <!-- Summary for the network but no internet connection was detected. --> - <string name="wifi_no_internet_no_reconnect">No Internet Access Detected, won\'t automatically reconnect.</string> + <string name="wifi_no_internet_no_reconnect">Won\'t automatically connect</string> <!-- Summary for the remembered network but no internet connection was detected. --> - <string name="wifi_no_internet">No Internet Access.</string> + <string name="wifi_no_internet">No Internet access</string> <!-- Summary for saved networks --> <string name="saved_network">Saved by <xliff:g id="name">%1$s</xliff:g></string> diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java new file mode 100644 index 000000000000..4ec4f4ff9c9e --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib.drawer; + +public final class CategoryKey { + + // Activities in this category shows up in Settings homepage. + public static final String CATEGORY_HOMEPAGE = "com.android.settings.category.ia.homepage"; + + // Top level category. + public static final String CATEGORY_NETWORK = "com.android.settings.category.ia.wireless"; + public static final String CATEGORY_DEVICE = "com.android.settings.category.ia.device"; + public static final String CATEGORY_APPS = "com.android.settings.category.ia.apps"; + public static final String CATEGORY_BATTERY = "com.android.settings.category.ia.battery"; + public static final String CATEGORY_DISPLAY = "com.android.settings.category.ia.display"; + public static final String CATEGORY_SOUND = "com.android.settings.category.ia.sound"; + public static final String CATEGORY_STORAGE = "com.android.settings.category.ia.storage"; + public static final String CATEGORY_SECURITY = "com.android.settings.category.ia.security"; + public static final String CATEGORY_ACCOUNT = "com.android.settings.category.ia.accounts"; + public static final String CATEGORY_SYSTEM = "com.android.settings.category.ia.system"; +} diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryManager.java b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryManager.java new file mode 100644 index 000000000000..a8f286d774c6 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryManager.java @@ -0,0 +1,108 @@ +/** + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib.drawer; + +import android.content.ComponentName; +import android.content.Context; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Pair; + +import com.android.settingslib.applications.InterestingConfigChanges; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class CategoryManager { + + private static final String TAG = "CategoryManager"; + + private static CategoryManager sInstance; + + private final InterestingConfigChanges mInterestingConfigChanges; + + // Tile cache (key: <packageName, activityName>, value: tile) + private final Map<Pair<String, String>, Tile> mTileByComponentCache; + + // Tile cache (key: category key, value: category) + private final Map<String, DashboardCategory> mCategoryByKeyMap; + + private List<DashboardCategory> mCategories; + + public static CategoryManager get() { + if (sInstance == null) { + sInstance = new CategoryManager(); + } + return sInstance; + } + + CategoryManager() { + mInterestingConfigChanges = new InterestingConfigChanges(); + mTileByComponentCache = new ArrayMap<>(); + mCategoryByKeyMap = new ArrayMap<>(); + } + + public DashboardCategory getTilesByCategory(Context context, String categoryKey) { + tryInitCategories(context); + + final DashboardCategory category = mCategoryByKeyMap.get(categoryKey); + if (category == null) { + throw new IllegalStateException("Can't find category with key " + categoryKey); + } + return category; + } + + public List<DashboardCategory> getCategories(Context context) { + tryInitCategories(context); + return mCategories; + } + + public void reloadAllCategoriesForConfigChange(Context context) { + if (mInterestingConfigChanges.applyNewConfig(context.getResources())) { + mCategories = null; + tryInitCategories(context); + } + } + + public void updateCategoryFromBlacklist(Set<ComponentName> tileBlacklist) { + if (mCategories == null) { + Log.w(TAG, "Category is null, skipping blacklist update"); + } + for (int i = 0; i < mCategories.size(); i++) { + DashboardCategory category = mCategories.get(i); + for (int j = 0; j < category.tiles.size(); j++) { + Tile tile = category.tiles.get(j); + if (tileBlacklist.contains(tile.intent.getComponent())) { + category.tiles.remove(j--); + } + } + } + } + + private void tryInitCategories(Context context) { + if (mCategories == null) { + mTileByComponentCache.clear(); + mCategoryByKeyMap.clear(); + mCategories = TileUtils.getCategories(context, mTileByComponentCache, + false /* categoryDefinedInManifest */); + for (DashboardCategory category : mCategories) { + mCategoryByKeyMap.put(category.key, category); + } + } + } + +} diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java b/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java index 53be0e6c116c..3fc999fb903a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java @@ -16,15 +16,20 @@ package com.android.settingslib.drawer; +import android.content.ComponentName; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import android.util.Log; import java.util.ArrayList; import java.util.List; public class DashboardCategory implements Parcelable { + private static final String TAG = "DashboardCategory"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + /** * Title of the category that is shown to the user. */ @@ -74,6 +79,22 @@ public class DashboardCategory implements Parcelable { return tiles.get(n); } + public boolean containsComponent(ComponentName component) { + for (Tile tile : tiles) { + if (TextUtils.equals(tile.intent.getComponent().getClassName(), + component.getClassName())) { + if (DEBUG) { + Log.d(TAG, "category " + key + "contains component" + component); + } + return true; + } + } + if (DEBUG) { + Log.d(TAG, "category " + key + " does not contain component" + component); + } + return false; + } + @Override public int describeContents() { return 0; diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java index 05585e53e2a2..50867eb28e0a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java @@ -33,7 +33,6 @@ import android.os.UserManager; import android.provider.Settings; import android.support.annotation.VisibleForTesting; import android.support.v4.widget.DrawerLayout; -import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import android.util.Pair; @@ -64,12 +63,9 @@ public class SettingsDrawerActivity extends Activity { public static final String EXTRA_SHOW_MENU = "show_drawer_menu"; - private static List<DashboardCategory> sDashboardCategories; - private static HashMap<Pair<String, String>, Tile> sTileCache; // Serves as a temporary list of tiles to ignore until we heard back from the PM that they // are disabled. private static ArraySet<ComponentName> sTileBlacklist = new ArraySet<>(); - private static InterestingConfigChanges sConfigTracker; private final PackageReceiver mPackageReceiver = new PackageReceiver(); private final List<CategoryListener> mCategoryListeners = new ArrayList<>(); @@ -80,6 +76,15 @@ public class SettingsDrawerActivity extends Activity { private boolean mShowingMenu; private UserManager mUserManager; + // Remove below after new IA + @Deprecated + private static List<DashboardCategory> sDashboardCategories; + @Deprecated + private static HashMap<Pair<String, String>, Tile> sTileCache; + @Deprecated + private static InterestingConfigChanges sConfigTracker; + // Remove above after new IA + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -105,7 +110,9 @@ public class SettingsDrawerActivity extends Activity { mDrawerLayout = null; return; } - getDashboardCategories(); + if (!isDashboardFeatureEnabled()) { + getDashboardCategories(); + } setActionBar(toolbar); mDrawerAdapter = new SettingsDrawerAdapter(this); ListView listView = (ListView) findViewById(R.id.left_drawer); @@ -144,7 +151,11 @@ public class SettingsDrawerActivity extends Activity { filter.addDataScheme("package"); registerReceiver(mPackageReceiver, filter); - new CategoriesUpdater().execute(); + if (isDashboardFeatureEnabled()) { + new CategoriesUpdateTask().execute(); + } else { + new CategoriesUpdater().execute(); + } } final Intent intent = getIntent(); if (intent != null) { @@ -173,23 +184,23 @@ public class SettingsDrawerActivity extends Activity { if (componentName == null) { return false; } - // Look for a tile that has the same component as incoming intent - final List<DashboardCategory> categories = getDashboardCategories(); - for (DashboardCategory category : categories) { - for (Tile tile : category.tiles) { - if (TextUtils.equals(tile.intent.getComponent().getClassName(), - componentName.getClassName())) { - if (DEBUG) { - Log.d(TAG, "intent is for top level tile: " + tile.title); - } + if (isDashboardFeatureEnabled()) { + final DashboardCategory homepageCategories = CategoryManager.get() + .getTilesByCategory(this, CategoryKey.CATEGORY_HOMEPAGE); + return homepageCategories.containsComponent(componentName); + } else { + // Look for a tile that has the same component as incoming intent + final List<DashboardCategory> categories = getDashboardCategories(); + for (DashboardCategory category : categories) { + if (category.containsComponent(componentName)) { return true; } } + if (DEBUG) { + Log.d(TAG, "Intent is not for top level settings " + intent); + } + return false; } - if (DEBUG) { - Log.d(TAG, "Intent is not for top level settings " + intent); - } - return false; } public void addCategoryListener(CategoryListener listener) { @@ -255,7 +266,11 @@ public class SettingsDrawerActivity extends Activity { return; } // TODO: Do this in the background with some loading. - mDrawerAdapter.updateCategories(); + if (isDashboardFeatureEnabled()) { + mDrawerAdapter.updateHomepageCategories(); + } else { + mDrawerAdapter.updateCategories(); + } if (mDrawerAdapter.getCount() != 0) { mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); } else { @@ -343,13 +358,6 @@ public class SettingsDrawerActivity extends Activity { } } - public HashMap<Pair<String, String>, Tile> getTileCache() { - if (sTileCache == null) { - getDashboardCategories(); - } - return sTileCache; - } - public void onProfileTileOpen() { finish(); } @@ -368,7 +376,11 @@ public class SettingsDrawerActivity extends Activity { ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); - new CategoriesUpdater().execute(); + if (isDashboardFeatureEnabled()) { + new CategoriesUpdateTask().execute(); + } else { + new CategoriesUpdater().execute(); + } } } @@ -376,6 +388,10 @@ public class SettingsDrawerActivity extends Activity { void onCategoriesChanged(); } + /** + * @deprecated remove after new IA + */ + @Deprecated private class CategoriesUpdater extends AsyncTask<Void, Void, List<DashboardCategory>> { @Override protected List<DashboardCategory> doInBackground(Void... params) { @@ -408,10 +424,39 @@ public class SettingsDrawerActivity extends Activity { } } + private class CategoriesUpdateTask extends AsyncTask<Void, Void, Void> { + + private final CategoryManager mCategoryManager; + + public CategoriesUpdateTask() { + mCategoryManager = CategoryManager.get(); + } + + @Override + protected Void doInBackground(Void... params) { + mCategoryManager.reloadAllCategoriesForConfigChange(SettingsDrawerActivity.this); + return null; + } + + @Override + protected void onPostExecute(Void result) { + mCategoryManager.updateCategoryFromBlacklist(sTileBlacklist); + onCategoriesChanged(); + } + } + + protected boolean isDashboardFeatureEnabled() { + return false; + } + private class PackageReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - new CategoriesUpdater().execute(); + if (isDashboardFeatureEnabled()) { + new CategoriesUpdateTask().execute(); + } else { + new CategoriesUpdater().execute(); + } } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerAdapter.java b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerAdapter.java index 1d6197a9333f..e1216a1c7e18 100644 --- a/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerAdapter.java +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerAdapter.java @@ -37,6 +37,10 @@ public class SettingsDrawerAdapter extends BaseAdapter { mActivity = activity; } + /** + * @deprecated Remove after new IA + */ + @Deprecated void updateCategories() { List<DashboardCategory> categories = mActivity.getDashboardCategories(); mItems.clear(); @@ -64,6 +68,27 @@ public class SettingsDrawerAdapter extends BaseAdapter { notifyDataSetChanged(); } + public void updateHomepageCategories() { + DashboardCategory category = + CategoryManager.get().getTilesByCategory(mActivity, CategoryKey.CATEGORY_HOMEPAGE); + mItems.clear(); + // Spacer. + mItems.add(null); + Item tile = new Item(); + tile.label = mActivity.getString(R.string.home); + tile.icon = Icon.createWithResource(mActivity, R.drawable.home); + mItems.add(tile); + for (int j = 0; j < category.tiles.size(); j++) { + tile = new Item(); + Tile dashboardTile = category.tiles.get(j); + tile.label = dashboardTile.title; + tile.icon = dashboardTile.icon; + tile.tile = dashboardTile; + mItems.add(tile); + } + notifyDataSetChanged(); + } + public Tile getTile(int position) { return mItems.get(position) != null ? mItems.get(position).tile : null; } diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java b/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java index e70cc29a7744..b2ce13f9f30a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java @@ -113,8 +113,24 @@ public class TileUtils { private static final String SETTING_PKG = "com.android.settings"; + /** + * Build a list of DashboardCategory. Each category must be defined in manifest. + * eg: .Settings$DeviceSettings + * @deprecated + */ + @Deprecated public static List<DashboardCategory> getCategories(Context context, - HashMap<Pair<String, String>, Tile> cache) { + Map<Pair<String, String>, Tile> cache) { + return getCategories(context, cache, true /*categoryDefinedInManifest*/); + } + + /** + * Build a list of DashboardCategory. + * @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to + * represent this category (eg: .Settings$DeviceSettings) + */ + public static List<DashboardCategory> getCategories(Context context, + Map<Pair<String, String>, Tile> cache, boolean categoryDefinedInManifest) { final long startTime = System.currentTimeMillis(); boolean setup = Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0) != 0; @@ -134,11 +150,12 @@ public class TileUtils { getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false); } } + HashMap<String, DashboardCategory> categoryMap = new HashMap<>(); for (Tile tile : tiles) { DashboardCategory category = categoryMap.get(tile.category); if (category == null) { - category = createCategory(context, tile.category); + category = createCategory(context, tile.category, categoryDefinedInManifest); if (category == null) { Log.w(LOG_TAG, "Couldn't find category " + tile.category); continue; @@ -157,9 +174,21 @@ public class TileUtils { return categories; } - private static DashboardCategory createCategory(Context context, String categoryKey) { + /** + * Create a new DashboardCategory from key. + * + * @param context Context to query intent + * @param categoryKey The category key + * @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to + * represent this category (eg: .Settings$DeviceSettings) + */ + private static DashboardCategory createCategory(Context context, String categoryKey, + boolean categoryDefinedInManifest) { DashboardCategory category = new DashboardCategory(); category.key = categoryKey; + if (!categoryDefinedInManifest) { + return category; + } PackageManager pm = context.getPackageManager(); List<ResolveInfo> results = pm.queryIntentActivities(new Intent(categoryKey), 0); if (results.size() == 0) { @@ -204,6 +233,8 @@ public class TileUtils { ActivityInfo activityInfo = resolved.activityInfo; Bundle metaData = activityInfo.metaData; String categoryKey = defaultCategory; + + // Load category if (checkCategory && ((metaData == null) || !metaData.containsKey(EXTRA_CATEGORY_KEY)) && categoryKey == null) { Log.w(LOG_TAG, "Found " + resolved.activityInfo.name + " for intent " @@ -213,6 +244,7 @@ public class TileUtils { } else { categoryKey = metaData.getString(EXTRA_CATEGORY_KEY); } + Pair<String, String> key = new Pair<String, String>(activityInfo.packageName, activityInfo.name); Tile tile = addedCache.get(key); @@ -238,16 +270,6 @@ public class TileUtils { } } - private static DashboardCategory getCategory(List<DashboardCategory> target, - String categoryKey) { - for (DashboardCategory category : target) { - if (categoryKey.equals(category.key)) { - return category; - } - } - return null; - } - private static boolean updateTileData(Context context, Tile tile, ActivityInfo activityInfo, ApplicationInfo applicationInfo, PackageManager pm) { if (applicationInfo.isSystemApp()) { diff --git a/packages/SystemUI/plugin/ExamplePlugin/AndroidManifest.xml b/packages/SystemUI/plugin/ExamplePlugin/AndroidManifest.xml index bd2c71c38f5a..ff89bbcb455f 100644 --- a/packages/SystemUI/plugin/ExamplePlugin/AndroidManifest.xml +++ b/packages/SystemUI/plugin/ExamplePlugin/AndroidManifest.xml @@ -21,7 +21,15 @@ <uses-permission android:name="com.android.systemui.permission.PLUGIN" /> <application> - <service android:name=".SampleOverlayPlugin"> + <activity android:name=".PluginSettings" + android:label="@string/plugin_label"> + <intent-filter> + <action android:name="com.android.systemui.action.PLUGIN_SETTINGS" /> + </intent-filter> + </activity> + + <service android:name=".SampleOverlayPlugin" + android:label="@string/plugin_label"> <intent-filter> <action android:name="com.android.systemui.action.PLUGIN_OVERLAY" /> </intent-filter> diff --git a/packages/SystemUI/plugin/ExamplePlugin/res/layout/plugin_settings.xml b/packages/SystemUI/plugin/ExamplePlugin/res/layout/plugin_settings.xml new file mode 100644 index 000000000000..eb90283f08d0 --- /dev/null +++ b/packages/SystemUI/plugin/ExamplePlugin/res/layout/plugin_settings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +** Copyright 2016, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +--> + +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:textAppearance="?android:attr/textAppearanceLarge" + android:text="@string/plugin_settings_here" + android:gravity="center" /> diff --git a/packages/SystemUI/plugin/ExamplePlugin/res/values/strings.xml b/packages/SystemUI/plugin/ExamplePlugin/res/values/strings.xml new file mode 100644 index 000000000000..a0bfe849e5c6 --- /dev/null +++ b/packages/SystemUI/plugin/ExamplePlugin/res/values/strings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2016, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +--> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + + <string name="plugin_settings_here" translatable="false">Plugin settings go here</string> + <string name="plugin_label" translatable="false">Overlay Plugin</string> + +</resources> diff --git a/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/PluginSettings.java b/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/PluginSettings.java new file mode 100644 index 000000000000..cf39075d958f --- /dev/null +++ b/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/PluginSettings.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugin.testoverlayplugin; + +import android.annotation.Nullable; +import android.app.Activity; +import android.os.Bundle; + +/** + * DO NOT Reference Plugin interfaces here, this runs in the plugin APK's process + * and is only for modifying settings. + */ +public class PluginSettings extends Activity { + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.plugin_settings); + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/IntentButtonProvider.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/IntentButtonProvider.java new file mode 100644 index 000000000000..1b8efa79ad73 --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/IntentButtonProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins; + +import android.content.Intent; +import android.graphics.drawable.Drawable; + +/** + * An Intent Button represents a triggerable element in SysUI that consists of an + * Icon and an intent to trigger when it is activated (clicked, swiped, etc.). + */ +public interface IntentButtonProvider extends Plugin { + + public static final int VERSION = 1; + + public IntentButton getIntentButton(); + + public interface IntentButton { + public static class IconState { + public boolean isVisible = true; + public CharSequence contentDescription = null; + public Drawable drawable; + } + + public IconState getIcon(); + + public Intent getIntent(); + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java index 2a7139c3a74c..495771a3c8ec 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java @@ -24,7 +24,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; -import android.os.AsyncTask; import android.os.Build; import android.os.Handler; import android.os.Looper; @@ -163,6 +162,7 @@ public class PluginInstanceManager<T extends Plugin> extends BroadcastReceiver { switch (msg.what) { case PLUGIN_CONNECTED: if (DEBUG) Log.d(TAG, "onPluginConnected"); + PluginPrefs.setHasPlugins(mContext); PluginInfo<T> info = (PluginInfo<T>) msg.obj; info.mPlugin.onCreate(mContext, info.mPluginContext); mListener.onPluginConnected(info.mPlugin); diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java index aa0b3c586747..4bf6494995bc 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java @@ -37,6 +37,7 @@ public class PluginManager { private final Context mContext; private final PluginInstanceManagerFactory mFactory; private final boolean isDebuggable; + private final PluginPrefs mPluginPrefs; private PluginManager(Context context) { this(context, new PluginInstanceManagerFactory(), Build.IS_DEBUGGABLE, @@ -51,6 +52,7 @@ public class PluginManager { mBackgroundThread = new HandlerThread("Plugins"); mBackgroundThread.start(); isDebuggable = debuggable; + mPluginPrefs = new PluginPrefs(mContext); PluginExceptionHandler uncaughtExceptionHandler = new PluginExceptionHandler( defaultHandler); @@ -68,6 +70,7 @@ public class PluginManager { // Never ever ever allow these on production builds, they are only for prototyping. return; } + mPluginPrefs.addAction(action); PluginInstanceManager p = mFactory.createPluginInstanceManager(mContext, action, listener, allowMultiple, mBackgroundThread.getLooper(), version); p.startListening(); diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginPrefs.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginPrefs.java new file mode 100644 index 000000000000..3671b3c1689f --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginPrefs.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.ArraySet; + +import java.util.Set; + +/** + * Storage for all plugin actions in SharedPreferences. + * + * This allows the list of actions that the Tuner needs to search for to be generated + * instead of hard coded. + */ +public class PluginPrefs { + + private static final String PREFS = "plugin_prefs"; + + private static final String PLUGIN_ACTIONS = "actions"; + private static final String HAS_PLUGINS = "plugins"; + + private final Set<String> mPluginActions; + private final SharedPreferences mSharedPrefs; + + public PluginPrefs(Context context) { + mSharedPrefs = context.getSharedPreferences(PREFS, 0); + mPluginActions = new ArraySet<>(mSharedPrefs.getStringSet(PLUGIN_ACTIONS, null)); + } + + public Set<String> getPluginList() { + return mPluginActions; + } + + public synchronized void addAction(String action) { + if (mPluginActions.add(action)){ + mSharedPrefs.edit().putStringSet(PLUGIN_ACTIONS, mPluginActions).commit(); + } + } + + public static boolean hasPlugins(Context context) { + return context.getSharedPreferences(PREFS, 0).getBoolean(HAS_PLUGINS, false); + } + + public static void setHasPlugins(Context context) { + context.getSharedPreferences(PREFS, 0).edit().putBoolean(HAS_PLUGINS, true).commit(); + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java new file mode 100644 index 000000000000..09879d895a22 --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins.statusbar.phone; + +import android.annotation.DrawableRes; +import android.annotation.Nullable; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.view.ViewGroup; + +import com.android.systemui.plugins.Plugin; + +public interface NavBarButtonProvider extends Plugin { + + public static final String ACTION = "com.android.systemui.action.PLUGIN_NAV_BUTTON"; + + public static final int VERSION = 1; + + /** + * Returns a view in the nav bar. If the id is set "back", "home", "recent_apps", "menu", + * or "ime_switcher", it is expected to implement ButtonInterface. + */ + public View createView(String spec, ViewGroup parent); + + /** + * Interface for button actions. + */ + interface ButtonInterface { + void setImageResource(@DrawableRes int resId); + + void setImageDrawable(@Nullable Drawable drawable); + + void abortCurrentGesture(); + + void setLandscape(boolean landscape); + + void setCarMode(boolean carMode); + } +} diff --git a/packages/SystemUI/res/layout/tuner_widget_settings_switch.xml b/packages/SystemUI/res/layout/tuner_widget_settings_switch.xml new file mode 100644 index 000000000000..c89c02fd9171 --- /dev/null +++ b/packages/SystemUI/res/layout/tuner_widget_settings_switch.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2016 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_vertical"> + + <ImageView + android:id="@+id/settings" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/ic_settings" + android:tint="@android:color/black" + android:padding="12dp" + android:background="?android:attr/selectableItemBackgroundBorderless" /> + + <View + android:id="@+id/divider" + android:layout_width="1dp" + android:layout_height="30dp" + android:layout_marginEnd="8dp" + android:background="?android:attr/listDivider" /> + + <Switch + android:id="@android:id/switch_widget" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:focusable="false" + android:clickable="false" + android:background="@null" /> +</LinearLayout> diff --git a/packages/SystemUI/res/values-af/strings.xml b/packages/SystemUI/res/values-af/strings.xml index ec77d77871e9..9fc0b6ec19f8 100644 --- a/packages/SystemUI/res/values-af/strings.xml +++ b/packages/SystemUI/res/values-af/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G-data is laat wag"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Sellulêre data is onderbreek"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Data is onderbreek"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Omdat die gestelde dataperk bereik is, het die toestel datagebruik vir die res van hierdie siklus onderbreek.\n\nAs dit hervat word, kan dit tot heffings deur jou diensverskaffer lei."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Hervat"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Geen internetverbinding nie"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi gekoppel"</string> diff --git a/packages/SystemUI/res/values-am/strings.xml b/packages/SystemUI/res/values-am/strings.xml index 9e00b7a97d18..0e49ab969530 100644 --- a/packages/SystemUI/res/values-am/strings.xml +++ b/packages/SystemUI/res/values-am/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4ጂ ውሂብ ላፍታ ቆሟል"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"የተንቀሳቃሽ ስልክ ውሂብ ላፍታ ቆሟል"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"ውሂብ ላፍታ ቆሟል"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"የእርስዎ የተዋቀረው የውሂብ ገደብ ላይ ስለተደረሰ፣ የዚህን ዑደት አጠቃቀም ለማስታወስ መሣሪያው ላፍታ ቆሟል።\n\nከቆመበት ማስቀጠሉ ከእርስዎ የአገልግሎት አቅራቢ ክፍያን ሊያስጠይቅዎት ይችላል።"</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"ከቆመበት ቀጥል"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"ምንም በይነመረብ ተያያዥ የለም።"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi ተያይዟል"</string> diff --git a/packages/SystemUI/res/values-ar/strings.xml b/packages/SystemUI/res/values-ar/strings.xml index 93c46472923a..abd32b7b47fd 100644 --- a/packages/SystemUI/res/values-ar/strings.xml +++ b/packages/SystemUI/res/values-ar/strings.xml @@ -241,7 +241,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"تم إيقاف بيانات شبكة الجيل الرابع مؤقتًا"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"تم إيقاف بيانات شبكة الجوّال مؤقتًا"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"تم إيقاف البيانات مؤقتًا"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"نظرًا لأنك بلغت الحد الأقصى المحدد للبيانات، فقد أوقف الجهاز استخدام البيانات مؤقتًا في بقية هذه الدورة.\n\nومن الممكن أن يؤدي الاستئناف إلى تحصيل رسوم من قِبل مشغِّل شبكة الجوّال."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"استئناف"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"لا يوجد اتصال إنترنت"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi متصل"</string> diff --git a/packages/SystemUI/res/values-az-rAZ/strings.xml b/packages/SystemUI/res/values-az-rAZ/strings.xml index 109610823eb6..ca5479404425 100644 --- a/packages/SystemUI/res/values-az-rAZ/strings.xml +++ b/packages/SystemUI/res/values-az-rAZ/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G məlumatlarına fasilə verildi"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobil məlumatlara fasilə verildi"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Məlumatlara fasilə verildi"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Məlumatlar dəsti limitinizi keçdiyiniz üçün cihaz bu dövrənin qalan hissəsi üçün məlumatların istifadəsinə fasilə verib.\n\nDavam etmək operaturunuzdan xərclərə səbəb ola bilər."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Davam et"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"İnternet bağlantısı yoxdur"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi qoşulub"</string> diff --git a/packages/SystemUI/res/values-b+sr+Latn/strings.xml b/packages/SystemUI/res/values-b+sr+Latn/strings.xml index d184130eb4e4..d22928df1957 100644 --- a/packages/SystemUI/res/values-b+sr+Latn/strings.xml +++ b/packages/SystemUI/res/values-b+sr+Latn/strings.xml @@ -238,7 +238,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G podaci su pauzirani"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobilni podaci su pauzirani"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Podaci su pauzirani"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Zbog toga što ste dostigli podešeno ograničenje za podatke, uređaj je pauzirao korišćenje podataka tokom ostatka ovog ciklusa.\n\nAko nastavite, mobilni operater može da vam naplati dodatne troškove."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Nastavi"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Nema internet veze"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi je povezan"</string> diff --git a/packages/SystemUI/res/values-be-rBY/strings.xml b/packages/SystemUI/res/values-be-rBY/strings.xml index cc71580e0452..c215680d58d3 100644 --- a/packages/SystemUI/res/values-be-rBY/strings.xml +++ b/packages/SystemUI/res/values-be-rBY/strings.xml @@ -241,7 +241,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Перадача даных 4G прыпынена"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Мабільная перадача даных прыпынена"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Перадача даных прыпынена"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Быў дасягнуты ліміт перадачы даных, таму прылада прыпыніла перадачу даных на астатнюю частку гэтага цыкла.\n\nУзнаўленне перадачы можа прывесці да спагнання платы вашым аператарам."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Узнавіць"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Няма падключэння да Iнтэрнэту"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi падключаны"</string> diff --git a/packages/SystemUI/res/values-bg/strings.xml b/packages/SystemUI/res/values-bg/strings.xml index 42ac038bcb08..c2a2a9241ee3 100644 --- a/packages/SystemUI/res/values-bg/strings.xml +++ b/packages/SystemUI/res/values-bg/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Данните от 4G са поставени на пауза"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Мобилните данни са поставени на пауза"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Данните са поставени на пауза"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Тъй като зададеното от вас ограничение за данни бе достигнато, устройството постави преноса им на пауза за остатъка от този цикъл.\n\nВъзобновяването може да доведе до таксуване от оператора ви."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Възобновяване"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Няма връзка с интернет"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi: Има връзка"</string> diff --git a/packages/SystemUI/res/values-bn-rBD/strings.xml b/packages/SystemUI/res/values-bn-rBD/strings.xml index 1467eea320ae..5ed264e417c3 100644 --- a/packages/SystemUI/res/values-bn-rBD/strings.xml +++ b/packages/SystemUI/res/values-bn-rBD/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G ডেটা বিরতি দেওয়া হয়েছে"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"সেলুলার ডেটা বিরতি দেওয়া হয়েছে"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"ডেট বিরতি দেওয়া হয়েছে"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"আপনার সেট ডেটার সীমা অবধি পৌঁছনোর কারনে ডিভাইস এই চক্রের অবশিষ্টাংশের জন্য ডেটা ব্যবহারে বিরতি দেওয়া হয়েছে৷ \n\nপুনরায় চালু করা হলে পরিষেবা প্রদানকারীর দ্বারা চার্জের করা হতে পারে৷"</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"পুনঃসূচনা করুন"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"কোনো ইন্টারনেট সংযোগ নেই"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"ওয়াই-ফাই সংযুক্ত হয়েছে"</string> diff --git a/packages/SystemUI/res/values-bs-rBA/strings.xml b/packages/SystemUI/res/values-bs-rBA/strings.xml index 349098a6655c..c621ecd3b0d5 100644 --- a/packages/SystemUI/res/values-bs-rBA/strings.xml +++ b/packages/SystemUI/res/values-bs-rBA/strings.xml @@ -238,7 +238,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G prijenos podataka je pauzirano"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobilni podaci su pauzirani"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Prijenos podataka je pauziran"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Dostigli ste postavljeno ograničenje prijenosa podataka pa je uređaj zaustavio prijenos podataka za preostali dio ovog ciklusa.\n\nAko nastavite, operater vam može naplatiti dodatne troškove."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Nastavi"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Nema internet veze"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi veza aktivna"</string> diff --git a/packages/SystemUI/res/values-ca/strings.xml b/packages/SystemUI/res/values-ca/strings.xml index b764bf63f3cd..882c8e34d6bd 100644 --- a/packages/SystemUI/res/values-ca/strings.xml +++ b/packages/SystemUI/res/values-ca/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Les dades 4G estan aturades"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Les dades mòbils estan aturades"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Les dades estan aturades"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Com que has arribat al límit de dades establert, s\'ha aturat l\'ús de dades del dispositiu per a la resta d\'aquest cicle.\n\nSi el reprens, l\'operador de telefonia mòbil pot aplicar càrrecs."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Reprèn"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"No hi ha connexió a Internet"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi: connectada"</string> diff --git a/packages/SystemUI/res/values-cs/strings.xml b/packages/SystemUI/res/values-cs/strings.xml index a829fe79949a..216e763072df 100644 --- a/packages/SystemUI/res/values-cs/strings.xml +++ b/packages/SystemUI/res/values-cs/strings.xml @@ -241,7 +241,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Data 4G jsou pozastavena"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobilní data jsou pozastavena"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Data jsou pozastavena"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Protože jste dosáhli nastaveného limitu dat, zařízení využití dat pro zbytek tohoto cyklu pozastavilo.\n\nObnovení může vést k poplatkům od operátora."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Pokračovat"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Žádné přip. k internetu"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi: připojeno"</string> diff --git a/packages/SystemUI/res/values-da/strings.xml b/packages/SystemUI/res/values-da/strings.xml index 64046e76d1af..77727bbd5ebe 100644 --- a/packages/SystemUI/res/values-da/strings.xml +++ b/packages/SystemUI/res/values-da/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G-data er sat på pause"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobildata er sat på pause"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Data er sat på pause"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Eftersom din fastsatte datagrænse blev nået, har enheden sat dataforbruget på pause i den resterende del af cyklussen.\n\nHvis du genaktiverer dataforbruget, kan det medføre gebyrer fra dit mobilselskab."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Genoptag"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Ingen internetforb."</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi er forbundet"</string> diff --git a/packages/SystemUI/res/values-de/strings.xml b/packages/SystemUI/res/values-de/strings.xml index 134e41fd2685..5aa99a14b14a 100644 --- a/packages/SystemUI/res/values-de/strings.xml +++ b/packages/SystemUI/res/values-de/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G-Daten pausiert"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobilfunkdaten pausiert"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Daten pausiert"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Da dein festgelegtes Datenlimit erreicht wurde, hat das Gerät die Datennutzung für den Rest dieses Zeitraums pausiert.\n\nWenn du die Nutzung fortsetzt, entstehen möglicherweise Kosten bei deinem Mobilfunkanbieter."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Fortsetzen"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Keine Internetverbindung"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"WLAN verbunden"</string> diff --git a/packages/SystemUI/res/values-el/strings.xml b/packages/SystemUI/res/values-el/strings.xml index b1085431a9ec..4c5718b7ccb5 100644 --- a/packages/SystemUI/res/values-el/strings.xml +++ b/packages/SystemUI/res/values-el/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Τα δεδομένα 4G τέθηκαν σε παύση"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Τα δεδομένα κινητής τηλεφωνίας τέθηκαν σε παύση"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Τα δεδομένα τέθηκαν σε παύση"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Επειδή συμπληρώθηκε το όριο των δεδομένων που ορίστηκε για τη συσκευή σας, η χρήση δεδομένων τέθηκε σε παύση για το υπόλοιπο αυτού του κύκλου.\n\nΗ εκ νέου ενεργοποίησή τους ενδέχεται να επιφέρει χρεώσεις από την εταιρεία κινητής τηλεφωνίας σας."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Συνέχιση"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Χωρ. σύνδ. στο Διαδ."</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi συνδεδεμένο"</string> diff --git a/packages/SystemUI/res/values-en-rAU/strings.xml b/packages/SystemUI/res/values-en-rAU/strings.xml index 776e043089f9..d12b44887d2a 100644 --- a/packages/SystemUI/res/values-en-rAU/strings.xml +++ b/packages/SystemUI/res/values-en-rAU/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G data is paused"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobile data is paused"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Data is paused"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Because your set data limit was reached, the device has paused data usage for the remainder of this cycle.\n\nResuming may lead to charges from your operator."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Resume"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"No Internet connection"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi connected"</string> diff --git a/packages/SystemUI/res/values-en-rGB/strings.xml b/packages/SystemUI/res/values-en-rGB/strings.xml index 776e043089f9..d12b44887d2a 100644 --- a/packages/SystemUI/res/values-en-rGB/strings.xml +++ b/packages/SystemUI/res/values-en-rGB/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G data is paused"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobile data is paused"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Data is paused"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Because your set data limit was reached, the device has paused data usage for the remainder of this cycle.\n\nResuming may lead to charges from your operator."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Resume"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"No Internet connection"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi connected"</string> diff --git a/packages/SystemUI/res/values-en-rIN/strings.xml b/packages/SystemUI/res/values-en-rIN/strings.xml index 776e043089f9..d12b44887d2a 100644 --- a/packages/SystemUI/res/values-en-rIN/strings.xml +++ b/packages/SystemUI/res/values-en-rIN/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G data is paused"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobile data is paused"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Data is paused"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Because your set data limit was reached, the device has paused data usage for the remainder of this cycle.\n\nResuming may lead to charges from your operator."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Resume"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"No Internet connection"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi connected"</string> diff --git a/packages/SystemUI/res/values-es-rUS/strings.xml b/packages/SystemUI/res/values-es-rUS/strings.xml index abaeafee420c..45198cfff959 100644 --- a/packages/SystemUI/res/values-es-rUS/strings.xml +++ b/packages/SystemUI/res/values-es-rUS/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Datos 4G pausados"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Datos móviles pausados"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Datos pausados"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Debido que se alcanzó el límite de datos establecido, el dispositivo pausó el uso de datos para el resto de este ciclo.\n\nLa reanudación podría tener como resultado cargos del proveedor."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Reanudar"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Sin conexión a Internet"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi conectado"</string> diff --git a/packages/SystemUI/res/values-es/strings.xml b/packages/SystemUI/res/values-es/strings.xml index bfb37b60b461..3d69e5ee3a9c 100644 --- a/packages/SystemUI/res/values-es/strings.xml +++ b/packages/SystemUI/res/values-es/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Datos 4G pausados"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Datos móviles pausados"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Datos pausados"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Has alcanzado el límite de datos establecido, por lo que el dispositivo ha pausado el uso de datos para el resto de este ciclo.\n\nSi lo reanudas, el operador puede facturar cargos."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Reanudar"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Sin conexión a Internet"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Con conexión Wi-Fi"</string> diff --git a/packages/SystemUI/res/values-et-rEE/strings.xml b/packages/SystemUI/res/values-et-rEE/strings.xml index ac55422d325a..17cbc1378302 100644 --- a/packages/SystemUI/res/values-et-rEE/strings.xml +++ b/packages/SystemUI/res/values-et-rEE/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G andmekasutus on peatatud"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobiilse andmeside kasutus on peatatud"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Andmekasutus on peatatud"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Kuna jõudsite andmemahu määratud piirini, peatas seade andmekasutuse ülejäänud tsükliks.\n\nJätkamisel võivad lisanduda operaatoritasud."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Jätka"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Interneti-ühendus puudub"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"WiFi on ühendatud"</string> diff --git a/packages/SystemUI/res/values-eu-rES/strings.xml b/packages/SystemUI/res/values-eu-rES/strings.xml index e0912e5b2aee..5715c506e8e8 100644 --- a/packages/SystemUI/res/values-eu-rES/strings.xml +++ b/packages/SystemUI/res/values-eu-rES/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G datuen erabilera eten da"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Sare mugikorreko datuen erabilera eten da"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Datuen erabilera eten da"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Zehaztuta duzun datuen erabilera-mugara iritsi zarenez, gailuak datuen erabilera eten du zikloa amaitzen den arte.\n\nDatuak erabiltzen jarraitzen baduzu, gastu gehiago ordaindu beharko dizkiozu agian operadoreari."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Jarraitu erabiltzen"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Ez duzu Interneteko konexiorik"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi konektatuta"</string> diff --git a/packages/SystemUI/res/values-fa/strings.xml b/packages/SystemUI/res/values-fa/strings.xml index df8ba8cc102b..d92b4c03a2de 100644 --- a/packages/SystemUI/res/values-fa/strings.xml +++ b/packages/SystemUI/res/values-fa/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"داده 4G موقتاً متوقف شده است"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"داده شبکه همراه موقتاً متوقف شده است"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"داده موقتاً متوقف شده است"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"چون به محدودیت داده تنظیم شده رسیدهاید، دستگاه مصرف داده را برای باقیمانده این دوره موقتاً متوقف کرده است.\n\nاگر ادامه دهید شاید موجب کسر هزینه از طرف شرکت مخابراتی شما شود."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"از سرگیری"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"اتصال اینترنتی ندارید"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi متصل شد"</string> diff --git a/packages/SystemUI/res/values-fi/strings.xml b/packages/SystemUI/res/values-fi/strings.xml index d15747293242..7bb7dd68b07f 100644 --- a/packages/SystemUI/res/values-fi/strings.xml +++ b/packages/SystemUI/res/values-fi/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G-tiedonsiirto keskeytettiin"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobiilitiedonsiirto keskeytettiin"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Tiedonsiirto keskeytettiin"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Määrittämäsi tiedonsiirtorajoitus saavutettiin, ja laite keskeytti tiedonsiirron tämän kauden loppuajaksi.\n\nOperaattorisi voi veloittaa sinulta lisämaksun, jos jatkat tiedonsiirtoa."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Jatka"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Ei internetyhteyttä"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi yhdistetty"</string> diff --git a/packages/SystemUI/res/values-fr-rCA/strings.xml b/packages/SystemUI/res/values-fr-rCA/strings.xml index 465de963f491..b0215bd149e5 100644 --- a/packages/SystemUI/res/values-fr-rCA/strings.xml +++ b/packages/SystemUI/res/values-fr-rCA/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Données 4G désactivées"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Données cellulaires désactivées"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Données désactivées"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Vous avez atteint le quota de données maximal. La consommation des données a donc été interrompue pour la fin de la période de facturation en cours.\n\nSi vous réactivez les données, votre fournisseur de services risque de vous facturer des frais supplémentaires."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Reprendre"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Aucune connexion Internet"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Connecté au Wi-Fi"</string> diff --git a/packages/SystemUI/res/values-fr/strings.xml b/packages/SystemUI/res/values-fr/strings.xml index 3ed8977aee9f..2a37637681cf 100644 --- a/packages/SystemUI/res/values-fr/strings.xml +++ b/packages/SystemUI/res/values-fr/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Données 4G désactivées"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Données mobiles désactivées"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Données désactivées"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Vous avez atteint le quota de données maximal. La consommation des données a donc été interrompue pour la fin de la période de facturation en cours.\n\nSi vous réactivez les données, votre opérateur risque de vous facturer des frais supplémentaires."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Réactiver"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Aucune connexion Internet"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Connecté au Wi-Fi"</string> diff --git a/packages/SystemUI/res/values-gl-rES/strings.xml b/packages/SystemUI/res/values-gl-rES/strings.xml index dafe79d172fe..40c5ca7a0c1b 100644 --- a/packages/SystemUI/res/values-gl-rES/strings.xml +++ b/packages/SystemUI/res/values-gl-rES/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Os datos 4G están en pausa"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Os datos de móbiles están en pausa"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Os datos están en pausa"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Como acadaches o límite de datos definido, o dispositivo puxo en pausa o uso de datos para o resto do ciclo.\n\nSe retomas o uso, poden aplicarse cargos do operador."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Retomar"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Sen Internet"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi conectada"</string> diff --git a/packages/SystemUI/res/values-gu-rIN/strings.xml b/packages/SystemUI/res/values-gu-rIN/strings.xml index b230bac7e554..5b3c9c2c0e14 100644 --- a/packages/SystemUI/res/values-gu-rIN/strings.xml +++ b/packages/SystemUI/res/values-gu-rIN/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G ડેટા થોભાવ્યો છે"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"સેલ્યુલર ડેટા થોભાવ્યો છે"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"ડેટા થોભાવ્યો છે"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"તમે સેટ કરેલ ડેટા મર્યાદા સુધી પહોંચી ગયા હોવાથી, ઉપકરણે આ ચક્રના શેષ માટે ડેટા વપરાશ થોભાવ્યો છે.\n\nફરીથી શરૂ કરવું તમારા કેરીઅર તરફથી શુલ્ક તરફ દોરી શકે છે."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"ફરી શરૂ કરો"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"કોઈ ઇન્ટરનેટ કનેક્શન નથી"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi કનેક્ટ કર્યું"</string> diff --git a/packages/SystemUI/res/values-hi/strings.xml b/packages/SystemUI/res/values-hi/strings.xml index c781403c0235..217a5f184b3e 100644 --- a/packages/SystemUI/res/values-hi/strings.xml +++ b/packages/SystemUI/res/values-hi/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G डेटा रोक दिया गया है"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"सेल्युलर डेटा रोक दिया गया है"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"डेटा रोक दिया गया है"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"चूंकि आपके निर्धारित डेटा की सीमा, सीमा पर पहुंच गई थी, इसलिए डिवाइस ने इस चक्र के रिमाइंडर के लिए डेटा उपयोग को रोक दिया है.\n\nफिर से शुरू करने से आपके वाहक की ओर से शुल्क लगाया जाता है."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"फिर से शुरू करें"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"कोई इंटरनेट कनेक्शन नहीं"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"वाई-फ़ाई कनेक्ट किया गया"</string> diff --git a/packages/SystemUI/res/values-hr/strings.xml b/packages/SystemUI/res/values-hr/strings.xml index 1de9f783a1d8..a9815d5fe4ee 100644 --- a/packages/SystemUI/res/values-hr/strings.xml +++ b/packages/SystemUI/res/values-hr/strings.xml @@ -238,7 +238,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G podaci pauzirani"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobilni podaci pauzirani"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Podaci su pauzirani"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Budući da je dosegnuto postavljeno ograničenje podataka, uređaj je pauzirao upotrebu podataka za preostali dio ovog ciklusa.\n\nMobilni operater može naplatiti daljnju upotrebu."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Nastavi"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Nema internetske veze"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi povezan"</string> diff --git a/packages/SystemUI/res/values-hu/strings.xml b/packages/SystemUI/res/values-hu/strings.xml index 6d699addfcb2..9fd4eb720145 100644 --- a/packages/SystemUI/res/values-hu/strings.xml +++ b/packages/SystemUI/res/values-hu/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"A 4G adatforgalom szünetel"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"A mobilhálózati adatforgalom szünetel"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Az adatforgalom szünetel"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Mivel elérte a beállított adatkorlátot, az eszköz a ciklus fennmaradó részére felfüggesztette az adathasználatot.\n\nHa mégis használja az adatkapcsolatot, akkor szolgáltatója többletköltséget számíthat fel."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Folytatás"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Nincs internet"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi csatlakoztatva"</string> diff --git a/packages/SystemUI/res/values-hy-rAM/strings.xml b/packages/SystemUI/res/values-hy-rAM/strings.xml index e08ec4638247..236663de0081 100644 --- a/packages/SystemUI/res/values-hy-rAM/strings.xml +++ b/packages/SystemUI/res/values-hy-rAM/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4Գ տվյալների օգտագործումը դադարեցված է"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Բջջային տվյալների օգտագործումը դադարեցված է"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Տվյալների օգտագործումը դադարեցված է"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Քանի որ ձեր սահմանված տվյալների սահմանաչափը սպառվել է, սարքն այլևս չի օգտագործի տվյալները այս ցիկլի մնացած ընթացքում:\n\nԵթե վերսկսեք, հնարավոր է կիրառվեն գանձումներ ձեր օպերատորի կողմից:"</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Վերսկսել"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Ինտերնետ կապ չկա"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi-ը միացված է"</string> diff --git a/packages/SystemUI/res/values-in/strings.xml b/packages/SystemUI/res/values-in/strings.xml index de4b68d9237b..5e2600808305 100644 --- a/packages/SystemUI/res/values-in/strings.xml +++ b/packages/SystemUI/res/values-in/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Data 4G dijeda"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Data seluler dijeda"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Data dijeda"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Karena batas data yang disetel telah tercapai, perangkat telah menjeda penggunaan data selama sisa waktu siklus ini.\n\nMelanjutkan dapat mengakibatkan tagihan dari operator."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Lanjutkan"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Tidak ada sambungan internet"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi tersambung"</string> diff --git a/packages/SystemUI/res/values-is-rIS/strings.xml b/packages/SystemUI/res/values-is-rIS/strings.xml index 9b1e13920ee3..02b59b613ba1 100644 --- a/packages/SystemUI/res/values-is-rIS/strings.xml +++ b/packages/SystemUI/res/values-is-rIS/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Slökkt er á 4G-gögnum"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Slökkt er á farsímagögnum"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Slökkt er á gagnanotkun"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Þar sem gagnahámarkinu var náð hefur tækið slökkt á gagnanotkun það sem eftir er af þessu tímabili.\n\nEf þú heldur áfram kann það að leiða til kostnaðar frá símafyrirtækinu."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Halda áfram"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Engin nettenging"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi tengt"</string> diff --git a/packages/SystemUI/res/values-it/strings.xml b/packages/SystemUI/res/values-it/strings.xml index f207e43f1171..9d76477efb14 100644 --- a/packages/SystemUI/res/values-it/strings.xml +++ b/packages/SystemUI/res/values-it/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Dati 4G sospesi"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Dati cellulari sospesi"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Dati sospesi"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Hai raggiunto il tuo limite di dati, pertanto sul dispositivo è stato sospeso l\'utilizzo di dati per la parte rimanente del ciclo.\n\nSe riprendi a utilizzare i dati, l\'operatore potrebbe addebitarti costi."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Riprendi"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Nessuna connessione"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi connesso"</string> diff --git a/packages/SystemUI/res/values-iw/strings.xml b/packages/SystemUI/res/values-iw/strings.xml index 6462ae5eb3ed..34294973455b 100644 --- a/packages/SystemUI/res/values-iw/strings.xml +++ b/packages/SystemUI/res/values-iw/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"השימוש בנתוני 4G מושהה"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"השימוש בנתונים סלולריים מושהה"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"השימוש בנתונים מושהה"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"מכיוון שהגעת למגבלת הנתונים שהגדרת, המכשיר השהה את השימוש בנתונים עד סוף התקופה.\n\nאם תמשיך, אתה עשוי לקבל חיובים מהספק."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"המשך"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"אין חיבור לאינטרנט"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi מחובר"</string> diff --git a/packages/SystemUI/res/values-ja/strings.xml b/packages/SystemUI/res/values-ja/strings.xml index fd59bc5b31db..bbd803353171 100644 --- a/packages/SystemUI/res/values-ja/strings.xml +++ b/packages/SystemUI/res/values-ja/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4Gデータは一時停止中です"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"モバイルデータは一時停止中です"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"データの一時停止"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"設定されたデータの上限に達したため、このサイクルの終了までこの端末でのデータの利用を一時停止しました。\n\n再開すると、携帯通信会社から課金される可能性があります。"</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"再開"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"インターネット未接続"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi接続済み"</string> diff --git a/packages/SystemUI/res/values-ka-rGE/strings.xml b/packages/SystemUI/res/values-ka-rGE/strings.xml index 28e37cad6a50..c00c54caf22a 100644 --- a/packages/SystemUI/res/values-ka-rGE/strings.xml +++ b/packages/SystemUI/res/values-ka-rGE/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G მონაცემები შეჩერებულია"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"ფიჭური მონაცემები შეჩერებულია"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"მონაცემები შეჩერებულია"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"რადგან თქვენი მონაცემების ლიმიტი ამოწურულია, მოწყობილობამ შეაჭერა მონაცემების გამოყენება დარჩენილი ციკლისათვის. \n\n შეჯამაბ შეიძლება გამოიწვიოს თქვენს პროვაიდერთან დამატებითი ხარჯები."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"გაგრძელება"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"ინტერნეტ კავშირი არ არის"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi დაკავშირებულია"</string> diff --git a/packages/SystemUI/res/values-kk-rKZ/strings.xml b/packages/SystemUI/res/values-kk-rKZ/strings.xml index ad25a1c89814..8051cd8d6dbe 100644 --- a/packages/SystemUI/res/values-kk-rKZ/strings.xml +++ b/packages/SystemUI/res/values-kk-rKZ/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G деректері кідіртілді"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Ұялы деректер кідіртілді"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Деректер кідіртілді"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Орнатылған деректер шегіне жеткендіктен, құрылғы осы циклдың қалған бөлігі бойы деректерді пайдалануды кідіртті.\n\nЖалғастыру оператор ақыларына әкелуі мүмкін."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Жалғастыру"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Интернет байланысы жоқ"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi қосулы"</string> diff --git a/packages/SystemUI/res/values-km-rKH/strings.xml b/packages/SystemUI/res/values-km-rKH/strings.xml index 76a8f59f215c..c0d002c4920e 100644 --- a/packages/SystemUI/res/values-km-rKH/strings.xml +++ b/packages/SystemUI/res/values-km-rKH/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"ទិន្នន័យ 4G ត្រូវបានផ្អាក"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"ទិន្នន័យចល័តត្រូវបានផ្អាក"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"ទិន្នន័យត្រូវបានផ្អាក"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"ដោយសារទិន្នន័យរបស់អ្នកបានឈានដល់កំណត់ ឧបករណ៍នេះបានផ្អាកការប្រើប្រាស់ទិន្នន័យសម្រាប់ការរំលឹកនៃវគ្គនេះ។\n\nការបន្តប្រើប្រាស់អាចនាំឲ្យមានការគិតប្រាក់ពីក្រុមហ៊ុនផ្តល់សេវាកម្ម។"</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"បន្ត"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"គ្មានការតភ្ជាប់អ៊ីនធឺណិត"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"បានភ្ជាប់វ៉ាយហ្វាយ"</string> diff --git a/packages/SystemUI/res/values-kn-rIN/strings.xml b/packages/SystemUI/res/values-kn-rIN/strings.xml index 69e3bdb55f5b..e1c42d794680 100644 --- a/packages/SystemUI/res/values-kn-rIN/strings.xml +++ b/packages/SystemUI/res/values-kn-rIN/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G ಡೇಟಾ ವಿರಾಮಗೊಳಿಸಲಾಗಿದೆ"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"ಸೆಲ್ಯುಲಾರ್ ಡೇಟಾ ವಿರಾಮಗೊಳಿಸಲಾಗಿದೆ"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"ಡೇಟಾ ವಿರಾಮಗೊಳಿಸಲಾಗಿದೆ"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"ಏಕೆಂದರೆ ನಿಮ್ಮ ಹೊಂದಾಣಿಕೆ ಡೇಟಾ ಮೀತಿಯನ್ನು ತಲುಪಿದೆ, ಈ ಆವರ್ತನೆಯ ಉಳಿದ ಭಾಗಕ್ಕೆ ಸಾಧನವು ಡೇಟಾ ಬಳಕೆಯನ್ನು ವಿರಾಮಗೊಳಿಸಿದೆ.\n\nಮುಂದುವರೆಯುವಿಕೆಯು ನಿಮ್ಮ ವಾಹಕದ ಶುಲ್ಕಗಳಿಗೆ ಕಾರಣವಾಗಬಹುದು."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"ಮುಂದುವರಿಸು"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"ಇಂಟರ್ನೆಟ್ ಸಂಪರ್ಕವಿಲ್ಲ"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"ವೈ-ಫೈ ಸಂಪರ್ಕಗೊಂಡಿದೆ"</string> diff --git a/packages/SystemUI/res/values-ko/strings.xml b/packages/SystemUI/res/values-ko/strings.xml index d47741202a01..1c9721261c7c 100644 --- a/packages/SystemUI/res/values-ko/strings.xml +++ b/packages/SystemUI/res/values-ko/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G 데이터 사용 중지됨"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"모바일 데이터 사용 중지됨"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"데이터 사용 중지됨"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"설정된 데이터 한도에 도달했기 때문에 기기에서 사이클의 나머지 기간 동안 데이터 사용을 일시 중지했습니다. \n\n데이터 사용을 재개하면 이동통신사 요금이 청구될 수 있습니다."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"재개"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"인터넷에 연결되지 않음"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi 연결됨"</string> diff --git a/packages/SystemUI/res/values-ky-rKG/strings.xml b/packages/SystemUI/res/values-ky-rKG/strings.xml index b84bea2008b9..b17f523de29e 100644 --- a/packages/SystemUI/res/values-ky-rKG/strings.xml +++ b/packages/SystemUI/res/values-ky-rKG/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G дайындары тындырылды"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Уюлдук дайындар тындырылды"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Дайындар тындырылды"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Киргизиле турган дайындар белгиленген эң жогорку чекке жеткендиктен, ушул мерчимдин калган бөлүгүндө түзмөгүңүздө дайындардын колдонулушу тындырылды.\n\nУлантсаңыз, байланыш операторуңузга акы төлөп калышыңыз мүмкүн."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Улантуу"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Интернет байланыш жок"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi байланышта"</string> diff --git a/packages/SystemUI/res/values-lo-rLA/strings.xml b/packages/SystemUI/res/values-lo-rLA/strings.xml index f1be8b3daf63..0dc3d50a8eff 100644 --- a/packages/SystemUI/res/values-lo-rLA/strings.xml +++ b/packages/SystemUI/res/values-lo-rLA/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"ຂໍ້ມູນ 4G ຢຸດຊົ່ວຄາວແລ້ວ"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"ຂໍ້ມູນເຊວລູລາຢຸດຊົ່ວຄາວແລ້ວ"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"ຂໍ້ມູນຢຸດຊົ່ວຄາວແລ້ວ"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"ເນື່ອງຈາກວ່າຮອດຂີດຈຳກັດຂໍ້ມູນທີ່ຕັ້ງໄວ້ຂອງທ່ານແລ້ວ, ອຸປະກອນຢຸດການນຳໃຊ້ຂໍ້ມູນສຳລັບສ່ວນທີ່ຍັງເຫຼືອຂອງຮອບວຽນນີ້.\n\nການເລີ່ມຕໍ່ອາດຈະນຳໄປສູ່ການປ່ຽນແປງຈາກຜູ້ໃຫ້ບໍລິການເຄືອຂ່າຍຂອງທ່ານ."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"ເລີ່ມຕໍ່"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"ບໍ່ມີການເຊື່ອມຕໍ່ອິນເຕີເນັດ"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"ເຊື່ອມຕໍ່ Wi--Fi ແລ້ວ"</string> diff --git a/packages/SystemUI/res/values-lt/strings.xml b/packages/SystemUI/res/values-lt/strings.xml index 8a0f956b521f..dd2f198355c5 100644 --- a/packages/SystemUI/res/values-lt/strings.xml +++ b/packages/SystemUI/res/values-lt/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G duomenys pristabdyti"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Korinio ryšio duomenys pristabdyti"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Duomenys pristabdyti"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Kadangi buvo pasiektas nustatytas duomenų limitas, įrenginys pristabdė duomenų naudojimą likusį šio ciklo laikotarpį.\n\nAtnaujinus gali būti taikomi operatoriaus mokesčiai."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Atnaujinti"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Nėra interneto ryš."</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Prisij. prie „Wi-Fi“"</string> diff --git a/packages/SystemUI/res/values-lv/strings.xml b/packages/SystemUI/res/values-lv/strings.xml index 76d0da4df8ea..42424978ba71 100644 --- a/packages/SystemUI/res/values-lv/strings.xml +++ b/packages/SystemUI/res/values-lv/strings.xml @@ -238,7 +238,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G datu lietojums ir apturēts"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobilo datu lietojums ir apturēts"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Datu lietojums ir apturēts"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Tika sasniegts iestatītais datu lietojuma ierobežojums, tādēļ ierīcē ir apturēts datu lietojums cikla atlikušajā periodā.\n\nJa atsāksiet lietot datus, iespējams, jūsu mobilo sakaru operators iekasēs maksu."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Atsākt"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Nav interneta sav."</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Izv. sav. ar Wi-Fi"</string> diff --git a/packages/SystemUI/res/values-mk-rMK/strings.xml b/packages/SystemUI/res/values-mk-rMK/strings.xml index d7982c7f2467..4091bbe3e1e6 100644 --- a/packages/SystemUI/res/values-mk-rMK/strings.xml +++ b/packages/SystemUI/res/values-mk-rMK/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Податоците 4G се паузирани"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Мобилните податоци се паузирани"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Податоците се паузирани"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Поради тоа што го достигнавте поставеното ограничување на податоци, уредот го паузираше користењето податоци до крајот на циклусот.\n\nОператорот може да ви наплати ако продолжите."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Продолжи"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Нема интернет"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Поврзано на Wi-Fi"</string> diff --git a/packages/SystemUI/res/values-ml-rIN/strings.xml b/packages/SystemUI/res/values-ml-rIN/strings.xml index 34aaa92ea172..6ae547e256aa 100644 --- a/packages/SystemUI/res/values-ml-rIN/strings.xml +++ b/packages/SystemUI/res/values-ml-rIN/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G ഡാറ്റ താൽക്കാലികമായി നിർത്തി"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"സെല്ലുലാർ ഡാറ്റ താൽക്കാലികമായി നിർത്തി"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"ഡാറ്റ താൽക്കാലികമായി നിർത്തി"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"നിങ്ങൾ നേരത്തെ ക്രമീകരിച്ച ഡാറ്റ പരിധിയിലെത്തിയതിനാൽ, ഈ സൈക്കിളിന്റെ അവശേഷിക്കുന്ന ഡാറ്റ ഉപയോഗം, ഉപകരണം താൽക്കാലികമായി നിർത്തി.\n\nപുനരാരംഭിക്കുന്നത്, നിങ്ങളുടെ കാരിയറിൽ നിന്ന് നിരക്കുകൾക്ക് ഇടയാക്കാം."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"പുനരാരംഭിക്കുക"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"ഇന്റർനെറ്റ് കണക്ഷൻ ഇല്ല"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"വൈഫൈ കണക്റ്റുചെയ്തു"</string> diff --git a/packages/SystemUI/res/values-mn-rMN/strings.xml b/packages/SystemUI/res/values-mn-rMN/strings.xml index 44f012527b48..95cbcaeccbdf 100644 --- a/packages/SystemUI/res/values-mn-rMN/strings.xml +++ b/packages/SystemUI/res/values-mn-rMN/strings.xml @@ -235,7 +235,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G дата-г түр зогсоосон байна"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Гар утасны дата-г түр зогсоосон байна"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Дата-г түр зогсоосон байна"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Таны багц дата эрхийн дээд хэмжээнд хүрсэн байгаа тул төхөөрөмж нь үлдсэн хэсэгт дата хэрэглээг түр зогсоосон байна.\n\nТа үйлдлийг үргэлжлүүлэхийг хүсвэл үйлчилгээ үзүүлж буй үүрэн холбооны газраас нэмж дата эрх авна уу."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Үргэлжлүүлэх"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Интернет холболт байхгүй"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi холбогдсон"</string> diff --git a/packages/SystemUI/res/values-mr-rIN/strings.xml b/packages/SystemUI/res/values-mr-rIN/strings.xml index eeb16c742516..830cda4b1a77 100644 --- a/packages/SystemUI/res/values-mr-rIN/strings.xml +++ b/packages/SystemUI/res/values-mr-rIN/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G डेटास विराम दिला आहे"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"सेल्युलर डेटास विराम दिला आहे"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"डेटास विराम दिला आहे"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"आपली सेट केलेली डेटा मर्यादा गाठल्यामुळे, डिव्हाइसने या चक्राच्या उर्वरित डेटा वापरास विराम दिला आहे.\n\nपुन्हा सुरु करण्यामुळे आपल्या वाहकाकडून शुल्क आकारले जाऊ शकते."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"पुन्हा सुरु करा"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"इंटरनेट कनेक्शन नाही"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"वाय-फाय कनेक्ट केले"</string> diff --git a/packages/SystemUI/res/values-ms-rMY/strings.xml b/packages/SystemUI/res/values-ms-rMY/strings.xml index 45215b4f9839..82de7a33986b 100644 --- a/packages/SystemUI/res/values-ms-rMY/strings.xml +++ b/packages/SystemUI/res/values-ms-rMY/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Data 4G dijeda"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Data selular dijeda"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Data dijeda"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Oleh kerana had data tetap anda telah dicapai, peranti telah menjeda penggunaan data bagi baki kitaran ini.\n\nMenyambung semula boleh menyebabkan anda dikenakan bayaran daripada pembawa anda."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Sambung semula"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Tiada smbg Internet"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi disambungkan"</string> diff --git a/packages/SystemUI/res/values-my-rMM/strings.xml b/packages/SystemUI/res/values-my-rMM/strings.xml index 31efa644754d..3899b00c7279 100644 --- a/packages/SystemUI/res/values-my-rMM/strings.xml +++ b/packages/SystemUI/res/values-my-rMM/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G data ခေတ္တရပ်တန့်သည်"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"cellular data ခေတ္တရပ်တန့်သည်"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"ဒေတာ ခေတ္တရပ်တန့်သည်"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"သင့် ဒေတာ အသုံးပြုမှု သတ်မှတ်ထားချက်သို့ ရောက်ရှိသောကြောင့်၊ ဤကာလအတွက် ကျန်ရှိသည့် ဒေတာအသုံးပြုမှုအား စက်ပစ္စည်းမှ ရပ်တန့်ထားသည်။\n\nဆက်လက်သွားပါက သင့်ဖုန်းဝန်ဆောင်မှုမှ သင့်အား ကုန်ကျစရိတ်တောင်းခံလိမ့်မည်။"</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"ပြန်ဆက်လုပ်ရန်"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"အင်တာနက်မရှိ"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"ကြိုးမဲ့ဆက်သွယ်မှု"</string> diff --git a/packages/SystemUI/res/values-nb/strings.xml b/packages/SystemUI/res/values-nb/strings.xml index 9d13ebf86ed4..0a4e0452ae46 100644 --- a/packages/SystemUI/res/values-nb/strings.xml +++ b/packages/SystemUI/res/values-nb/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G-data er satt på pause"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobildata er satt på pause"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Data er satt på pause"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Fordi den angitte datagrensen ble nådd, har enheten satt databruk på pause for resten av denne syklusen. \n\nHvis du gjenopptar bruken, kan det føre til avgifter fra operatøren din."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Gjenoppta"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Ingen Internett-forbindelse"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi tilkoblet"</string> diff --git a/packages/SystemUI/res/values-ne-rNP/strings.xml b/packages/SystemUI/res/values-ne-rNP/strings.xml index 0a37810008ee..47d326c60f31 100644 --- a/packages/SystemUI/res/values-ne-rNP/strings.xml +++ b/packages/SystemUI/res/values-ne-rNP/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G डेटा रोकिएको छ"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"सेल्यूलर डेटा रोकिएको छ"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"डेटा रोकिएको छ"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"तपाईंले सेट गर्नुभएको डेटाको सीमा पुगेकाले, यन्त्रले यस चक्रको बाँकी भागका लागि डेटाको प्रयोग रोकेको छ।\n\nपुन: सुरू गर्दा तपाईंको क्यारियरले शुल्कहरू लिन सक्छ।"</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"पुनः सुरु गर्नुहोस्"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"इन्टरनेट जडान छैन"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi जडित"</string> diff --git a/packages/SystemUI/res/values-nl/strings.xml b/packages/SystemUI/res/values-nl/strings.xml index ef55b3c18596..49319d166243 100644 --- a/packages/SystemUI/res/values-nl/strings.xml +++ b/packages/SystemUI/res/values-nl/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G-data zijn onderbroken"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobiele gegevens zijn onderbroken"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Gegevens zijn onderbroken"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Omdat de ingestelde gegevenslimiet is bereikt, heeft het apparaat het gegevensverbruik onderbroken voor de rest van deze cyclus.\n\nAls u het gegevensverbruik hervat, kan je provider kosten in rekening brengen."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Hervatten"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Geen internetverbinding"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Verbonden via wifi"</string> diff --git a/packages/SystemUI/res/values-pa-rIN/strings.xml b/packages/SystemUI/res/values-pa-rIN/strings.xml index 635e96e056bb..082ff3a2b256 100644 --- a/packages/SystemUI/res/values-pa-rIN/strings.xml +++ b/packages/SystemUI/res/values-pa-rIN/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G ਡੈਟਾ ਰੁਕ ਗਿਆ ਹੈ"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"ਸੈਲਿਊਲਰ ਡੈਟਾ ਰੁਕ ਗਿਆ ਹੈ"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"ਡੈਟਾ ਰੁਕ ਗਿਆ ਹੈ"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"ਕਿਉਂਕਿ ਤੁਹਾਡੀ ਸੈਟ ਡੈਟਾ ਸੀਮਾ ਪੂਰੀ ਹੋ ਗਈ ਸੀ, ਡੀਵਾਈਸ ਨੇ ਇਸ ਬਾਕੀ ਚੱਕਰ ਲਈ ਡੈਟਾ ਉਪਯੋਗ ਰੋਕ ਦਿੱਤਾ ਹੈ।\n\nਇਸਨੂੰ ਦੁਬਾਰਾ ਸ਼ੁਰੂ ਕਰਨ ਨਾਲ ਤੁਹਾਡੇ ਕੈਰੀਅਰ ਵੱਲੋਂ ਖ਼ਰਚੇ ਪਾਏ ਜਾ ਸਕਦੇ ਹਨ।"</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"ਦੁਬਾਰਾ ਸ਼ੁਰੂ ਕਰੋ"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"ਕੋਈ ਇੰਟਰਨੈਟ ਕਨੈਕਸ਼ਨ ਨਹੀਂ"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi ਕਨੈਕਟ ਕੀਤਾ"</string> diff --git a/packages/SystemUI/res/values-pl/strings.xml b/packages/SystemUI/res/values-pl/strings.xml index 5d3fd99e7241..52bce7ce8648 100644 --- a/packages/SystemUI/res/values-pl/strings.xml +++ b/packages/SystemUI/res/values-pl/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Transmisja danych 4G została wstrzymana"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Komórkowa transmisja danych została wstrzymana"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Transmisja danych została wstrzymana"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Ponieważ został osiągnięty ustawiony przez Ciebie limit danych, urządzenie wstrzymało użycie danych na pozostałą część tego cyklu.\n\nWznowienie może spowodować naliczenie opłat przez operatora."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Wznów"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Brak internetu"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi: połączono"</string> diff --git a/packages/SystemUI/res/values-pt-rBR/strings.xml b/packages/SystemUI/res/values-pt-rBR/strings.xml index 298fc3cb93c5..5ecf074c1359 100644 --- a/packages/SystemUI/res/values-pt-rBR/strings.xml +++ b/packages/SystemUI/res/values-pt-rBR/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Os dados 4G foram pausados"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Os dados da rede celular foram pausados"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Os dados foram pausados"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Como seu limite de dados definido foi atingido, o dispositivo pausou o uso de dados para o restante deste ciclo.\n\nA retomada pode gerar cobranças por parte da sua operadora."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Retomar"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Sem conexão à Internet"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi conectado"</string> diff --git a/packages/SystemUI/res/values-pt-rPT/strings.xml b/packages/SystemUI/res/values-pt-rPT/strings.xml index 33176b6a464b..c1e662c159d7 100644 --- a/packages/SystemUI/res/values-pt-rPT/strings.xml +++ b/packages/SystemUI/res/values-pt-rPT/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Dados 4G em pausa"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Dados de redes móveis em pausa"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Dados em pausa"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Uma vez que foi atingido o limite de dados definido, foi interrompida a utilização de dados no seu dispositivo durante o tempo restante deste ciclo.\n\nSe retomar a utilização, o seu operador pode efetuar cobranças."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Retomar"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Sem ligação internet"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi ligado"</string> diff --git a/packages/SystemUI/res/values-pt/strings.xml b/packages/SystemUI/res/values-pt/strings.xml index 298fc3cb93c5..5ecf074c1359 100644 --- a/packages/SystemUI/res/values-pt/strings.xml +++ b/packages/SystemUI/res/values-pt/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Os dados 4G foram pausados"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Os dados da rede celular foram pausados"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Os dados foram pausados"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Como seu limite de dados definido foi atingido, o dispositivo pausou o uso de dados para o restante deste ciclo.\n\nA retomada pode gerar cobranças por parte da sua operadora."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Retomar"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Sem conexão à Internet"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi conectado"</string> diff --git a/packages/SystemUI/res/values-ro/strings.xml b/packages/SystemUI/res/values-ro/strings.xml index 9d6b7d902350..bedb7d0d1ff2 100644 --- a/packages/SystemUI/res/values-ro/strings.xml +++ b/packages/SystemUI/res/values-ro/strings.xml @@ -240,7 +240,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Conexiunea de date 4G este întreruptă"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Conexiunea de date mobile este întreruptă"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Conexiunea de date este întreruptă"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Deoarece limita setată pentru date a fost atinsă, dispozitivul a întrerupt utilizarea datelor pentru restul acestui ciclu.\n\nReluarea utilizării de date poate genera aplicarea de taxe de către operator."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Reluați"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Fără conex. internet"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi conectat"</string> diff --git a/packages/SystemUI/res/values-ru/strings.xml b/packages/SystemUI/res/values-ru/strings.xml index 9ac4155f757c..a1551042ed80 100644 --- a/packages/SystemUI/res/values-ru/strings.xml +++ b/packages/SystemUI/res/values-ru/strings.xml @@ -241,7 +241,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Передача данных 4G приостановлена"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Передача мобильных данных приостановлена"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Передача данных приостановлена"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Передача данных выключена до конца цикла, поскольку достигнут установленный вами лимит.\n\nЕсли вы возобновите ее, оператор может взимать плату за дополнительный расход трафика."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Возобновить"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Нет интернет-подключения"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi подключено"</string> diff --git a/packages/SystemUI/res/values-si-rLK/strings.xml b/packages/SystemUI/res/values-si-rLK/strings.xml index 95cac9e32a04..567d9e9dc424 100644 --- a/packages/SystemUI/res/values-si-rLK/strings.xml +++ b/packages/SystemUI/res/values-si-rLK/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G දත්ත විරාම කර ඇත"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"සෙලියුලර් දත්ත විරාම කර ඇත"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"දත්ත විරාම කර ඇත"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"ඔබ සකසා ඇති දත්ත සීමාවට ළඟා වූ නිසා, උපාංගය මගින් මෙම චක්රයේ ඉතිරිය සඳහා දත්ත භාවිතය විරාම කර ඇත. \n\nනැවත පටන් ගැනීමෙන් ඔබගේ වාහකයෙන් අය කිරීම් වලට පොළඹවනු ඇත."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"නැවත පටන්ගන්න"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"අන්තර්ජාල සම්බන්ධතාවයක් නැත"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi සම්බන්ධිතයි"</string> diff --git a/packages/SystemUI/res/values-sk/strings.xml b/packages/SystemUI/res/values-sk/strings.xml index 9c534c56e1f3..527a1fc3b546 100644 --- a/packages/SystemUI/res/values-sk/strings.xml +++ b/packages/SystemUI/res/values-sk/strings.xml @@ -241,7 +241,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Dátové prenosy 4G sú pozastavené"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobilné dáta sú pozastavené"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Dáta sú pozastavené"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Keďže ste dosiahli nastavený limit pre mobilné dáta, na zariadení bola pre zvyšok tohto cyklu pozastavená spotreba dát.\n\nJej opätovné spustenie môže mať za následok účtovanie poplatkov vaším operátorom."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Znova spustiť"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Bez prip. na Internet"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi: pripojené"</string> diff --git a/packages/SystemUI/res/values-sl/strings.xml b/packages/SystemUI/res/values-sl/strings.xml index d930a72d78be..3134962f48ad 100644 --- a/packages/SystemUI/res/values-sl/strings.xml +++ b/packages/SystemUI/res/values-sl/strings.xml @@ -241,7 +241,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Prenos podatkov v omrežju 4G je zaustavljen"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Prenos mobilnih podatkov je zaustavljen"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Prenos podatkov je zaustavljen"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Dosegli ste nastavljeno omejitev količine prenesenih podatkov, zato je naprava zaustavila porabo podatkov za preostanek cikla.\n\nČe nadaljujete s porabo, boste morda morali plačati stroške operaterju."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Nadaljuj"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Ni internetne povez."</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi povezan"</string> diff --git a/packages/SystemUI/res/values-sq-rAL/strings.xml b/packages/SystemUI/res/values-sq-rAL/strings.xml index 7c11eb970e19..afa7694af5d6 100644 --- a/packages/SystemUI/res/values-sq-rAL/strings.xml +++ b/packages/SystemUI/res/values-sq-rAL/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Të dhënat 4G janë ndërprerë"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Të dhënat celulare janë ndërprerë"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Të dhënat janë ndërprerë"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Pajisja jote ka ndërprerë përdorimin e të dhënave për pjesën e mbetur të ciklit sepse është arritur kufiri i caktuar i të dhënave.\n\nVazhdimi mund të sjellë tarifa nga operatori celular."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Rifillo"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Nuk ka lidhje interneti"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi është i lidhur"</string> diff --git a/packages/SystemUI/res/values-sr/strings.xml b/packages/SystemUI/res/values-sr/strings.xml index 30c14680b1bc..06fabeedecf4 100644 --- a/packages/SystemUI/res/values-sr/strings.xml +++ b/packages/SystemUI/res/values-sr/strings.xml @@ -238,7 +238,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G подаци су паузирани"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Мобилни подаци су паузирани"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Подаци су паузирани"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Због тога што сте достигли подешено ограничење за податке, уређај је паузирао коришћење података током остатка овог циклуса.\n\nАко наставите, мобилни оператер може да вам наплати додатне трошкове."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Настави"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Нема интернет везе"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi је повезан"</string> diff --git a/packages/SystemUI/res/values-sv/strings.xml b/packages/SystemUI/res/values-sv/strings.xml index b574e90b30c9..8b110c513ffd 100644 --- a/packages/SystemUI/res/values-sv/strings.xml +++ b/packages/SystemUI/res/values-sv/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G-data har pausats"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobildata har pausats"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Dataanvändningen har pausats"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Eftersom du har nått den angivna datagränsen har dataanvändningen pausats under resten av perioden.\n\nOm du återupptar dataanvändningen kan avgifter från operatören tillkomma."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Återuppta"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Ingen anslutning"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi-ansluten"</string> diff --git a/packages/SystemUI/res/values-sw/strings.xml b/packages/SystemUI/res/values-sw/strings.xml index 4e9d628d7554..be91af39bcd8 100644 --- a/packages/SystemUI/res/values-sw/strings.xml +++ b/packages/SystemUI/res/values-sw/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Data ya 4G imesitishwa"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Data ya simu ya mkononi imesitishwa"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Data imesitishwa"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Kwa sababu ulifikia kiwango cha juu cha data kilichowekwa, kifaa kimesitisha matumizi ya data katika awamu hii iliyosalia.\n\n Kuendelea kunaweza kusababisha malipo kwa mtoa huduma wako."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Endelea"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Hakuna muunganisho wa mtandao"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Mtandao-hewa umeunganishwa"</string> diff --git a/packages/SystemUI/res/values-ta-rIN/strings.xml b/packages/SystemUI/res/values-ta-rIN/strings.xml index a2ed16f6929f..799d628e05f4 100644 --- a/packages/SystemUI/res/values-ta-rIN/strings.xml +++ b/packages/SystemUI/res/values-ta-rIN/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G டேட்டா இடைநிறுத்தப்பட்டது"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"செல்லுலார் தரவு இடைநிறுத்தப்பட்டது"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"தரவு இடைநிறுத்தப்பட்டது"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"அமைக்கப்பட்ட தரவின் வரம்பை அடைந்துவிட்டதால், இந்தச் சுழற்சியின் மீதமுள்ள நாட்களுக்கான தரவுப் பயன்பாட்டைச் சாதனம் இடைநிறுத்தியுள்ளது.\n\nமீண்டும் தொடங்குவது, மொபைல் நிறுவனக் கட்டணங்களுக்கு உட்படுத்தலாம்."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"மீண்டும் தொடங்கு"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"இணைய இணைப்பு இல்லை"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"வைஃபை இணைக்கப்பட்டது"</string> diff --git a/packages/SystemUI/res/values-te-rIN/strings.xml b/packages/SystemUI/res/values-te-rIN/strings.xml index fdb509265a09..f6e3ee23f399 100644 --- a/packages/SystemUI/res/values-te-rIN/strings.xml +++ b/packages/SystemUI/res/values-te-rIN/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G డేటా పాజ్ చేయబడింది"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"సెల్యులార్ డేటా పాజ్ చేయబడింది"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"డేటా పాజ్ చేయబడింది"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"మీ సెట్ చేయబడిన డేటా పరిమితిని చేరుకున్నందున పరికరం ఈ సైకిల్లో మిగిలిన భాగానికి డేటా వినియోగాన్ని పాజ్ చేసింది.\n\nపునఃప్రారంభించడం వలన మీ క్యారియర్ ఛార్జీలు విధించవచ్చు."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"పునఃప్రారంభించు"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"ఇంటర్నెట్ కనెక్షన్ లేదు"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi కనెక్ట్ చేయబడింది"</string> diff --git a/packages/SystemUI/res/values-th/strings.xml b/packages/SystemUI/res/values-th/strings.xml index 678937a9b1ec..0482e3bc92e6 100644 --- a/packages/SystemUI/res/values-th/strings.xml +++ b/packages/SystemUI/res/values-th/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"หยุดการใช้ข้อมูล 4G ชั่วคราวแล้ว"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"หยุดการใช้ข้อมูลมือถือชั่วคราวแล้ว"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"หยุดการใช้ข้อมูลชั่วคราวแล้ว"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"เนื่องจากใช้งานข้อมูลถึงขีดจำกัดที่กำหนดไว้แล้ว อุปกรณ์จึงหยุดการใช้งานข้อมูลไว้ชั่วคราวตลอดระยะเวลาที่เหลือของรอบนี้\n\nการทำให้กลับมาทำงานอีกครั้งอาจทำให้เกิดค่าใช้จ่ายจากผู้ให้บริการ"</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"ทำต่อ"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"ไม่มีอินเทอร์เน็ต"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"เชื่อมต่อ WiFi แล้ว"</string> diff --git a/packages/SystemUI/res/values-tl/strings.xml b/packages/SystemUI/res/values-tl/strings.xml index 50c7e5c153c4..9b104f5391ad 100644 --- a/packages/SystemUI/res/values-tl/strings.xml +++ b/packages/SystemUI/res/values-tl/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Naka-pause ang 4G data"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Naka-pause ang cellular data"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Naka-pause ang data"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Dahil naabot ang iyong nakatakdang limitasyon sa data, na-pause ng device ang paggamit ng data para sa nalalabing bahagi ng cycle na ito.\n\nMaaaring makakuha ng mga singilin mula sa iyong carrier ang pagpapatuloy."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Ipagpatuloy"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Walang koneksyon sa Internet"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"nakakonekta ang Wi-Fi"</string> diff --git a/packages/SystemUI/res/values-tr/strings.xml b/packages/SystemUI/res/values-tr/strings.xml index 575d011b905f..e655cd637bf7 100644 --- a/packages/SystemUI/res/values-tr/strings.xml +++ b/packages/SystemUI/res/values-tr/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G veri kullanımı duraklatıldı"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Hücresel veri kullanımı duraklatıldı"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Veri kullanımı duraklatıldı"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Ayarlanmış olan veri sınırınıza ulaşıldığından, bu dönemin kalan süresi için cihazda veri kullanımı duraklatıldı.\n\nVeri kullanımını devam ettirmek, operatörünüzün sizden ödeme almasına neden olabilir."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Devam ettir"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"İnternet bağlantısı yok"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Kablosuz bağlandı"</string> diff --git a/packages/SystemUI/res/values-uk/strings.xml b/packages/SystemUI/res/values-uk/strings.xml index 4d4162c5e45d..eccc4a27aa1b 100644 --- a/packages/SystemUI/res/values-uk/strings.xml +++ b/packages/SystemUI/res/values-uk/strings.xml @@ -241,7 +241,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Передавання даних 4G призупинено"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Передавання мобільних даних призупинено"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Передавання даних призупинено"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Пристрій призупинив передавання даних до кінця цього циклу, оскільки ваш ліміт перевищено.\n\nЯкщо ви відновите передавання даних, оператор може стягувати додаткову плату."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Відновити"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Немає з’єднання"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi під’єднано"</string> diff --git a/packages/SystemUI/res/values-ur-rPK/strings.xml b/packages/SystemUI/res/values-ur-rPK/strings.xml index 6a3e4db4b911..dc177d125c23 100644 --- a/packages/SystemUI/res/values-ur-rPK/strings.xml +++ b/packages/SystemUI/res/values-ur-rPK/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G ڈیٹا موقوف کر دیا گیا"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"سیلولر ڈیٹا موقوف کر دیا گیا"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"ڈیٹا موقوف کر دیا گیا"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"چونکہ آپ کی سیٹ کردہ ڈیٹا کی حد تک پہنچ گیا، لہذا آلہ نے اس سائیکل کے بقیہ حصے کیلئے ڈیٹا کے استعمال کو موقوف کر دیا ہے۔\n\nدوبارہ شروع کرنے سے آپ کے کیریئر سے چارجز لگ سکتے ہیں۔"</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"دوبارہ شروع کریں"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"کوئی انٹرنیٹ کنکشن نہیں"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi مربوط ہے"</string> diff --git a/packages/SystemUI/res/values-uz-rUZ/strings.xml b/packages/SystemUI/res/values-uz-rUZ/strings.xml index f7bae9be7402..32fb3030d587 100644 --- a/packages/SystemUI/res/values-uz-rUZ/strings.xml +++ b/packages/SystemUI/res/values-uz-rUZ/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G internet to‘xtatib qo‘yildi"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Mobil internetdan foydalanish to‘xtatib qo‘yildi"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Internetdan foydalanish to‘xtatib qo‘yildi"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Siz o‘rnatgan mobil internet chekloviga yetgani bois joriy hisob-kitob davrining qolgan muddati uchun mobil internetdan foydalanish vaqtinchalik to‘xtatib qo‘yildi.\n\nAgar internetdan foydalanishni davom ettirsangiz, buning uchun uyali aloqa operatoringiz ortiqcha haq talab qilishi mumkin."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Davom etish"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Internetga ulanmagan"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi ulandi"</string> diff --git a/packages/SystemUI/res/values-vi/strings.xml b/packages/SystemUI/res/values-vi/strings.xml index df6fdb931955..41467968e137 100644 --- a/packages/SystemUI/res/values-vi/strings.xml +++ b/packages/SystemUI/res/values-vi/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"Đã tạm dừng dữ liệu 4G"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Đã tạm dừng dữ liệu di động"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Đã tạm dừng dữ liệu"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Vì bạn đã đạt tới giới hạn dữ liệu thiết lập nên thiết bị đã tạm dừng sử dụng dữ liệu cho phần còn lại của chu kỳ này.\n\nTiếp tục có thể dẫn tới nhà cung cấp dịch vụ của bạn sẽ tính phí."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Tiếp tục"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Ko có k.nối Internet"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Đã kết nối Wi-Fi"</string> diff --git a/packages/SystemUI/res/values-zh-rCN/strings.xml b/packages/SystemUI/res/values-zh-rCN/strings.xml index 7bda7a0f10bd..30f624545465 100644 --- a/packages/SystemUI/res/values-zh-rCN/strings.xml +++ b/packages/SystemUI/res/values-zh-rCN/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G 数据网络已暂停使用"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"移动数据网络已暂停使用"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"数据网络已暂停使用"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"由于使用的数据流量已达到您所设置的上限,因此您的设备已暂停在此周期的剩余时间内使用数据流量。\n\n如果恢复数据流量使用,您的运营商可能会向您收取相应费用。"</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"恢复"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"未连接互联网"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"已连接到WLAN网络"</string> diff --git a/packages/SystemUI/res/values-zh-rHK/strings.xml b/packages/SystemUI/res/values-zh-rHK/strings.xml index 9dbf41f2956d..f00a30bf8f2b 100644 --- a/packages/SystemUI/res/values-zh-rHK/strings.xml +++ b/packages/SystemUI/res/values-zh-rHK/strings.xml @@ -239,7 +239,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"已暫停 4G 數據"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"已暫停流動數據"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"已暫停使用數據"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"由於您已達到設定的數據用量上限,裝置已暫停使用數據,直到週期結束。\n\n如恢復使用數據,流動網絡供應商可能會向您收取費用。"</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"恢復"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"沒有互聯網連線"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi 已連線"</string> diff --git a/packages/SystemUI/res/values-zh-rTW/strings.xml b/packages/SystemUI/res/values-zh-rTW/strings.xml index b06103738e8b..34d12ffb67a9 100644 --- a/packages/SystemUI/res/values-zh-rTW/strings.xml +++ b/packages/SystemUI/res/values-zh-rTW/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"已暫停 4G 數據連線"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"已暫停行動數據連線"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"已暫停數據連線"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"由於數據用量已達設定上限,裝置在這個週期的剩餘時間將暫停使用數據連線。\n\n如果恢復使用,行動通訊業者可能會向您收取額外的連線費用。"</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"恢復連線"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"沒有網際網路連線"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"Wi-Fi 已連線"</string> diff --git a/packages/SystemUI/res/values-zu/strings.xml b/packages/SystemUI/res/values-zu/strings.xml index f97e71a25c3a..1b2794e1d42b 100644 --- a/packages/SystemUI/res/values-zu/strings.xml +++ b/packages/SystemUI/res/values-zu/strings.xml @@ -237,7 +237,8 @@ <string name="data_usage_disabled_dialog_4g_title" msgid="1601769736881078016">"4G idatha imisiwe"</string> <string name="data_usage_disabled_dialog_mobile_title" msgid="4651001290947318931">"Idatha yeselula imisiwe"</string> <string name="data_usage_disabled_dialog_title" msgid="3932437232199671967">"Idatha imisiwe"</string> - <string name="data_usage_disabled_dialog" msgid="8453242888903772524">"Ngoba umkhawulo wakho wedatha osethiwe ufinyelelwe, idivayisi imise kancane ukusetshenziswa kwedatha ngesikhumbuzi salo mjikelezo.\n\nUkuqhuba futhi kungaholela kuzindleko kusuka kwinkampani yakho yenethiwekhi."</string> + <!-- no translation found for data_usage_disabled_dialog (1841738975235283398) --> + <skip /> <string name="data_usage_disabled_dialog_enable" msgid="1412395410306390593">"Qalisa kabusha"</string> <string name="status_bar_settings_signal_meter_disconnected" msgid="1940231521274147771">"Alukho uxhumano lwe-Inthanethi"</string> <string name="status_bar_settings_signal_meter_wifi_nossid" msgid="6557486452774597820">"I-Wi-Fi ixhunyiwe"</string> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 562fb7f62b83..3f485c3bad8b 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1667,4 +1667,8 @@ <!-- accessibility label for paging indicator in quick settings [CHAR LIMITi=NONE] --> <string name="accessibility_quick_settings_page">Page <xliff:g name="current_page" example="1">%1$d</xliff:g> of <xliff:g name="num_pages" example="2">%2$d</xliff:g></string> + <!-- Plugin control section of the tuner. Non-translatable since it should + not appear on production builds ever. --> + <string name="plugins" translatable="false">Plugins</string> + </resources> diff --git a/packages/SystemUI/res/xml/tuner_prefs.xml b/packages/SystemUI/res/xml/tuner_prefs.xml index b46e862471f3..211f8e8183e8 100644 --- a/packages/SystemUI/res/xml/tuner_prefs.xml +++ b/packages/SystemUI/res/xml/tuner_prefs.xml @@ -133,6 +133,11 @@ android:title="@string/other" android:fragment="com.android.systemui.tuner.OtherPrefs" /> + <Preference + android:key="plugins" + android:title="@string/plugins" + android:fragment="com.android.systemui.tuner.PluginFragment" /> + <!-- Warning, this goes last. --> <Preference android:summary="@string/tuner_persistent_warning" diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java index afedbe3ad21f..1c242e9b6092 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java @@ -88,9 +88,9 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { if (mListening == listening) return; mListening = listening; if (mListening) { - mPages.get(mPosition).setListening(listening); + setPageListening(mPosition, true); if (mOffPage) { - mPages.get(mPosition + 1).setListening(listening); + setPageListening(mPosition + 1, true); } } else { // Make sure no pages are listening. @@ -131,6 +131,9 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { private void setPageListening(int position, boolean listening) { if (position >= mPages.size()) return; + if (isLayoutRtl()) { + position = mPages.size() - 1 - position; + } mPages.get(position).setListening(listening); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java index 78e56c04ce9b..58d57f68d699 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java @@ -116,6 +116,8 @@ public class NotificationContentView extends FrameLayout { private boolean mForceSelectNextLayout = true; private PendingIntent mPreviousExpandedRemoteInputIntent; private PendingIntent mPreviousHeadsUpRemoteInputIntent; + private RemoteInputView mCachedExpandedRemoteInput; + private RemoteInputView mCachedHeadsUpRemoteInput; private int mContentHeightAtAnimationStart = UNDEFINED; private boolean mFocusOnVisibilityChange; @@ -298,6 +300,9 @@ public class NotificationContentView extends FrameLayout { mExpandedRemoteInput.onNotificationUpdateOrReset(); if (mExpandedRemoteInput.isActive()) { mPreviousExpandedRemoteInputIntent = mExpandedRemoteInput.getPendingIntent(); + mCachedExpandedRemoteInput = mExpandedRemoteInput; + mExpandedRemoteInput.dispatchStartTemporaryDetach(); + ((ViewGroup)mExpandedRemoteInput.getParent()).removeView(mExpandedRemoteInput); } } if (mExpandedChild != null) { @@ -310,6 +315,9 @@ public class NotificationContentView extends FrameLayout { mHeadsUpRemoteInput.onNotificationUpdateOrReset(); if (mHeadsUpRemoteInput.isActive()) { mPreviousHeadsUpRemoteInputIntent = mHeadsUpRemoteInput.getPendingIntent(); + mCachedHeadsUpRemoteInput = mHeadsUpRemoteInput; + mHeadsUpRemoteInput.dispatchStartTemporaryDetach(); + ((ViewGroup)mHeadsUpRemoteInput.getParent()).removeView(mHeadsUpRemoteInput); } } if (mHeadsUpChild != null) { @@ -963,22 +971,35 @@ public class NotificationContentView extends FrameLayout { View bigContentView = mExpandedChild; if (bigContentView != null) { mExpandedRemoteInput = applyRemoteInput(bigContentView, entry, hasRemoteInput, - mPreviousExpandedRemoteInputIntent); + mPreviousExpandedRemoteInputIntent, mCachedExpandedRemoteInput); } else { mExpandedRemoteInput = null; } + if (mCachedExpandedRemoteInput != null + && mCachedExpandedRemoteInput != mExpandedRemoteInput) { + // We had a cached remote input but didn't reuse it. Clean up required. + mCachedExpandedRemoteInput.dispatchFinishTemporaryDetach(); + } + mCachedExpandedRemoteInput = null; View headsUpContentView = mHeadsUpChild; if (headsUpContentView != null) { mHeadsUpRemoteInput = applyRemoteInput(headsUpContentView, entry, hasRemoteInput, - mPreviousHeadsUpRemoteInputIntent); + mPreviousHeadsUpRemoteInputIntent, mCachedHeadsUpRemoteInput); } else { mHeadsUpRemoteInput = null; } + if (mCachedHeadsUpRemoteInput != null + && mCachedHeadsUpRemoteInput != mHeadsUpRemoteInput) { + // We had a cached remote input but didn't reuse it. Clean up required. + mCachedHeadsUpRemoteInput.dispatchFinishTemporaryDetach(); + } + mCachedHeadsUpRemoteInput = null; } private RemoteInputView applyRemoteInput(View view, NotificationData.Entry entry, - boolean hasRemoteInput, PendingIntent existingPendingIntent) { + boolean hasRemoteInput, PendingIntent existingPendingIntent, + RemoteInputView cachedView) { View actionContainerCandidate = view.findViewById( com.android.internal.R.id.actions_container); if (actionContainerCandidate instanceof FrameLayout) { @@ -991,15 +1012,22 @@ public class NotificationContentView extends FrameLayout { if (existing == null && hasRemoteInput) { ViewGroup actionContainer = (FrameLayout) actionContainerCandidate; - RemoteInputView riv = RemoteInputView.inflate( - mContext, actionContainer, entry, mRemoteInputController); - - riv.setVisibility(View.INVISIBLE); - actionContainer.addView(riv, new LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT) - ); - existing = riv; + if (cachedView == null) { + RemoteInputView riv = RemoteInputView.inflate( + mContext, actionContainer, entry, mRemoteInputController); + + riv.setVisibility(View.INVISIBLE); + actionContainer.addView(riv, new LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT) + ); + existing = riv; + } else { + actionContainer.addView(cachedView); + cachedView.dispatchFinishTemporaryDetach(); + cachedView.requestFocus(); + existing = cachedView; + } } if (hasRemoteInput) { int color = entry.notification.getNotification().color; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java index 6cbaceaa00ff..66cc15d7cc8f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java @@ -21,7 +21,9 @@ import com.android.systemui.statusbar.phone.StatusBarWindowManager; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.RemoteInputView; +import android.util.ArrayMap; import android.util.ArraySet; +import android.util.Pair; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -31,8 +33,9 @@ import java.util.ArrayList; */ public class RemoteInputController { - private final ArrayList<WeakReference<NotificationData.Entry>> mOpen = new ArrayList<>(); - private final ArraySet<String> mSpinning = new ArraySet<>(); + private final ArrayList<Pair<WeakReference<NotificationData.Entry>, Object>> mOpen + = new ArrayList<>(); + private final ArrayMap<String, Object> mSpinning = new ArrayMap<>(); private final ArrayList<Callback> mCallbacks = new ArrayList<>(3); private final HeadsUpManager mHeadsUpManager; @@ -41,36 +44,72 @@ public class RemoteInputController { mHeadsUpManager = headsUpManager; } - public void addRemoteInput(NotificationData.Entry entry) { + /** + * Adds a currently active remote input. + * + * @param entry the entry for which a remote input is now active. + * @param token a token identifying the view that is managing the remote input + */ + public void addRemoteInput(NotificationData.Entry entry, Object token) { Preconditions.checkNotNull(entry); + Preconditions.checkNotNull(token); boolean found = pruneWeakThenRemoveAndContains( - entry /* contains */, null /* remove */); + entry /* contains */, null /* remove */, token /* removeToken */); if (!found) { - mOpen.add(new WeakReference<>(entry)); + mOpen.add(new Pair<>(new WeakReference<>(entry), token)); } apply(entry); } - public void removeRemoteInput(NotificationData.Entry entry) { + /** + * Removes a currently active remote input. + * + * @param entry the entry for which a remote input should be removed. + * @param token a token identifying the view that is requesting the removal. If non-null, + * the entry is only removed if the token matches the last added token for this + * entry. If null, the entry is removed regardless. + */ + public void removeRemoteInput(NotificationData.Entry entry, Object token) { Preconditions.checkNotNull(entry); - pruneWeakThenRemoveAndContains(null /* contains */, entry /* remove */); + pruneWeakThenRemoveAndContains(null /* contains */, entry /* remove */, token); apply(entry); } - public void addSpinning(String key) { - mSpinning.add(key); + /** + * Adds a currently spinning (i.e. sending) remote input. + * + * @param key the key of the entry that's spinning. + * @param token the token of the view managing the remote input. + */ + public void addSpinning(String key, Object token) { + Preconditions.checkNotNull(key); + Preconditions.checkNotNull(token); + + mSpinning.put(key, token); } - public void removeSpinning(String key) { - mSpinning.remove(key); + /** + * Removes a currently spinning remote input. + * + * @param key the key of the entry for which a remote input should be removed. + * @param token a token identifying the view that is requesting the removal. If non-null, + * the entry is only removed if the token matches the last added token for this + * entry. If null, the entry is removed regardless. + */ + public void removeSpinning(String key, Object token) { + Preconditions.checkNotNull(key); + + if (token == null || mSpinning.get(key) == token) { + mSpinning.remove(key); + } } public boolean isSpinning(String key) { - return mSpinning.contains(key); + return mSpinning.containsKey(key); } private void apply(NotificationData.Entry entry) { @@ -86,14 +125,16 @@ public class RemoteInputController { * @return true if {@param entry} has an active RemoteInput */ public boolean isRemoteInputActive(NotificationData.Entry entry) { - return pruneWeakThenRemoveAndContains(entry /* contains */, null /* remove */); + return pruneWeakThenRemoveAndContains(entry /* contains */, null /* remove */, + null /* removeToken */); } /** * @return true if any entry has an active RemoteInput */ public boolean isRemoteInputActive() { - pruneWeakThenRemoveAndContains(null /* contains */, null /* remove */); + pruneWeakThenRemoveAndContains(null /* contains */, null /* remove */, + null /* removeToken */); return !mOpen.isEmpty(); } @@ -101,17 +142,27 @@ public class RemoteInputController { * Prunes dangling weak references, removes entries referring to {@param remove} and returns * whether {@param contains} is part of the array in a single loop. * @param remove if non-null, removes this entry from the active remote inputs + * @param removeToken if non-null, only removes an entry if this matches the token when the + * entry was added. * @return true if {@param contains} is in the set of active remote inputs */ private boolean pruneWeakThenRemoveAndContains( - NotificationData.Entry contains, NotificationData.Entry remove) { + NotificationData.Entry contains, NotificationData.Entry remove, Object removeToken) { boolean found = false; for (int i = mOpen.size() - 1; i >= 0; i--) { - NotificationData.Entry item = mOpen.get(i).get(); - if (item == null || item == remove) { + NotificationData.Entry item = mOpen.get(i).first.get(); + Object itemToken = mOpen.get(i).second; + boolean removeTokenMatches = (removeToken == null || itemToken == removeToken); + + if (item == null || (item == remove && removeTokenMatches)) { mOpen.remove(i); } else if (item == contains) { - found = true; + if (removeToken != null && removeToken != itemToken) { + // We need to update the token. Remove here and let caller reinsert it. + mOpen.remove(i); + } else { + found = true; + } } } return found; @@ -138,7 +189,7 @@ public class RemoteInputController { // Make a copy because closing the remote inputs will modify mOpen. ArrayList<NotificationData.Entry> list = new ArrayList<>(mOpen.size()); for (int i = mOpen.size() - 1; i >= 0; i--) { - NotificationData.Entry item = mOpen.get(i).get(); + NotificationData.Entry item = mOpen.get(i).first.get(); if (item != null && item.row != null) { list.add(item); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java index cf87ddd17aae..a7331d8d3299 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java @@ -105,7 +105,7 @@ public class CarStatusBar extends PhoneStatusBar implements R.dimen.status_bar_connected_device_signal_margin_end)); mConnectedDeviceSignalController = new ConnectedDeviceSignalController(mContext, - mSignalsView); + mSignalsView, mBluetoothController); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "makeStatusBarView(). mBatteryMeterView: " + mBatteryMeterView); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java index 07856daa9d8a..66030b935f07 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java @@ -17,11 +17,15 @@ import android.view.View; import android.widget.ImageView; import com.android.systemui.R; import com.android.systemui.statusbar.ScalingDrawableWrapper; +import com.android.systemui.statusbar.policy.BluetoothController; + +import static com.android.systemui.statusbar.phone.PhoneStatusBar.DEBUG; /** * Controller that monitors signal strength for a device that is connected via bluetooth. */ -public class ConnectedDeviceSignalController extends BroadcastReceiver { +public class ConnectedDeviceSignalController extends BroadcastReceiver implements + BluetoothController.Callback { private final static String TAG = "DeviceSignalCtlr"; /** @@ -54,18 +58,21 @@ public class ConnectedDeviceSignalController extends BroadcastReceiver { private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter(); private final Context mContext; - private final View mSignalsView; + private final BluetoothController mController; + private final View mSignalsView; private final ImageView mNetworkSignalView; private final float mIconScaleFactor; private BluetoothHeadsetClient mBluetoothHeadsetClient; - public ConnectedDeviceSignalController(Context context, View signalsView) { + public ConnectedDeviceSignalController(Context context, View signalsView, + BluetoothController controller) { mContext = context; - mSignalsView = signalsView; + mController = controller; + mSignalsView = signalsView; mNetworkSignalView = (ImageView) mSignalsView.findViewById(R.id.connected_device_network_signal); @@ -86,22 +93,46 @@ public class ConnectedDeviceSignalController extends BroadcastReceiver { filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED); filter.addAction(BluetoothHeadsetClient.ACTION_AG_EVENT); mContext.registerReceiver(this, filter); + + mController.addStateChangedCallback(this); } public void stopListening() { mContext.unregisterReceiver(this); + mController.removeStateChangedCallback(this); + } + + @Override + public void onBluetoothDevicesChanged() { + // Nothing to do here because this Controller is not displaying a list of possible + // bluetooth devices. + } + + @Override + public void onBluetoothStateChange(boolean enabled) { + if (DEBUG) { + Log.d(TAG, "onBluetoothStateChange(). enabled: " + enabled); + } + + // Only need to handle the case if bluetooth has been disabled, in which case the + // signal indicators are hidden. If bluetooth has been enabled, then this class should + // receive updates to the connection state via onReceive(). + if (!enabled) { + mNetworkSignalView.setVisibility(View.GONE); + mSignalsView.setVisibility(View.GONE); + } } @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - if (Log.isLoggable(TAG, Log.DEBUG)) { + if (DEBUG) { Log.d(TAG, "onReceive(). action: " + action); } if (BluetoothHeadsetClient.ACTION_AG_EVENT.equals(action)) { - if (Log.isLoggable(TAG, Log.DEBUG)) { + if (DEBUG) { Log.d(TAG, "Received ACTION_AG_EVENT"); } @@ -109,13 +140,13 @@ public class ConnectedDeviceSignalController extends BroadcastReceiver { } else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); - if (Log.isLoggable(TAG, Log.DEBUG)) { + if (DEBUG) { int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1); Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED event: " + oldState + " -> " + newState); } BluetoothDevice device = - (BluetoothDevice)intent.getExtra(BluetoothDevice.EXTRA_DEVICE); + (BluetoothDevice) intent.getExtra(BluetoothDevice.EXTRA_DEVICE); updateViewVisibility(device, newState); } } @@ -128,7 +159,7 @@ public class ConnectedDeviceSignalController extends BroadcastReceiver { int networkStatus = intent.getIntExtra(BluetoothHeadsetClient.EXTRA_NETWORK_STATUS, INVALID_SIGNAL); if (networkStatus != INVALID_SIGNAL) { - if (Log.isLoggable(TAG, Log.DEBUG)) { + if (DEBUG) { Log.d(TAG, "EXTRA_NETWORK_STATUS: " + " " + networkStatus); } @@ -140,7 +171,7 @@ public class ConnectedDeviceSignalController extends BroadcastReceiver { int signalStrength = intent.getIntExtra( BluetoothHeadsetClient.EXTRA_NETWORK_SIGNAL_STRENGTH, INVALID_SIGNAL); if (signalStrength != INVALID_SIGNAL) { - if (Log.isLoggable(TAG, Log.DEBUG)) { + if (DEBUG) { Log.d(TAG, "EXTRA_NETWORK_SIGNAL_STRENGTH: " + signalStrength); } @@ -150,7 +181,7 @@ public class ConnectedDeviceSignalController extends BroadcastReceiver { int roamingStatus = intent.getIntExtra(BluetoothHeadsetClient.EXTRA_NETWORK_ROAMING, INVALID_SIGNAL); if (roamingStatus != INVALID_SIGNAL) { - if (Log.isLoggable(TAG, Log.DEBUG)) { + if (DEBUG) { Log.d(TAG, "EXTRA_NETWORK_ROAMING: " + roamingStatus); } } @@ -169,7 +200,7 @@ public class ConnectedDeviceSignalController extends BroadcastReceiver { private void updateViewVisibility(BluetoothDevice device, int newState) { if (newState == BluetoothProfile.STATE_CONNECTED) { - if (Log.isLoggable(TAG, Log.DEBUG)) { + if (DEBUG) { Log.d(TAG, "Device connected"); } @@ -186,14 +217,14 @@ public class ConnectedDeviceSignalController extends BroadcastReceiver { int signalStrength = featuresBundle.getInt( BluetoothHeadsetClient.EXTRA_NETWORK_SIGNAL_STRENGTH, INVALID_SIGNAL); if (signalStrength != INVALID_SIGNAL) { - if (Log.isLoggable(TAG, Log.DEBUG)) { + if (DEBUG) { Log.d(TAG, "EXTRA_NETWORK_SIGNAL_STRENGTH: " + signalStrength); } setNetworkSignalIcon(SIGNAL_STRENGTH_ICONS[signalStrength]); } } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { - if (Log.isLoggable(TAG, Log.DEBUG)) { + if (DEBUG) { Log.d(TAG, "Device disconnected"); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java index 95cb672666fe..f6fe17607539 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java @@ -14,11 +14,11 @@ package com.android.systemui.statusbar.phone; -import android.annotation.DrawableRes; -import android.annotation.Nullable; import android.graphics.drawable.Drawable; import android.view.View; +import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider.ButtonInterface; + import java.util.ArrayList; /** @@ -186,18 +186,4 @@ public class ButtonDispatcher { } } - /** - * Interface for button actions. - */ - public interface ButtonInterface { - void setImageResource(@DrawableRes int resId); - - void setImageDrawable(@Nullable Drawable drawable); - - void abortCurrentGesture(); - - void setLandscape(boolean landscape); - - void setCarMode(boolean carMode); - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java index 0a391ebda5be..427014714040 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java @@ -61,6 +61,11 @@ import com.android.systemui.EventLogTags; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.assist.AssistManager; +import com.android.systemui.plugins.IntentButtonProvider; +import com.android.systemui.plugins.IntentButtonProvider.IntentButton; +import com.android.systemui.plugins.IntentButtonProvider.IntentButton.IconState; +import com.android.systemui.plugins.PluginListener; +import com.android.systemui.plugins.PluginManager; import com.android.systemui.plugins.qs.QSContainer.ActivityStarter; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.KeyguardAffordanceView; @@ -86,6 +91,11 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL public static final String EXTRA_CAMERA_LAUNCH_SOURCE = "com.android.systemui.camera_launch_source"; + private static final String LEFT_BUTTON_PLUGIN + = "com.android.systemui.action.PLUGIN_LOCKSCREEN_LEFT_BUTTON"; + private static final String RIGHT_BUTTON_PLUGIN + = "com.android.systemui.action.PLUGIN_LOCKSCREEN_RIGHT_BUTTON"; + private static final Intent SECURE_CAMERA_INTENT = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE) .addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); @@ -95,7 +105,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL private static final int DOZE_ANIMATION_STAGGER_DELAY = 48; private static final int DOZE_ANIMATION_ELEMENT_DURATION = 250; - private KeyguardAffordanceView mCameraImageView; + private KeyguardAffordanceView mRightAffordanceView; private KeyguardAffordanceView mLeftAffordanceView; private LockIcon mLockIcon; private TextView mIndicationText; @@ -132,6 +142,9 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL private boolean mLeftIsVoiceAssist; private AssistManager mAssistManager; + private IntentButton mRightButton = new DefaultRightButton(); + private IntentButton mLeftButton = new DefaultLeftButton(); + public KeyguardBottomAreaView(Context context) { this(context, null); } @@ -156,7 +169,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL String label = null; if (host == mLockIcon) { label = getResources().getString(R.string.unlock_label); - } else if (host == mCameraImageView) { + } else if (host == mRightAffordanceView) { label = getResources().getString(R.string.camera_label); } else if (host == mLeftAffordanceView) { if (mLeftIsVoiceAssist) { @@ -175,7 +188,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL mPhoneStatusBar.animateCollapsePanels( CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */); return true; - } else if (host == mCameraImageView) { + } else if (host == mRightAffordanceView) { launchCamera(CAMERA_LAUNCH_SOURCE_AFFORDANCE); return true; } else if (host == mLeftAffordanceView) { @@ -192,7 +205,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL super.onFinishInflate(); mLockPatternUtils = new LockPatternUtils(mContext); mPreviewContainer = (ViewGroup) findViewById(R.id.preview_container); - mCameraImageView = (KeyguardAffordanceView) findViewById(R.id.camera_button); + mRightAffordanceView = (KeyguardAffordanceView) findViewById(R.id.camera_button); mLeftAffordanceView = (KeyguardAffordanceView) findViewById(R.id.left_button); mLockIcon = (LockIcon) findViewById(R.id.lock_icon); mIndicationText = (TextView) findViewById(R.id.keyguard_indication_text); @@ -207,15 +220,31 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL inflateCameraPreview(); mLockIcon.setOnClickListener(this); mLockIcon.setOnLongClickListener(this); - mCameraImageView.setOnClickListener(this); + mRightAffordanceView.setOnClickListener(this); mLeftAffordanceView.setOnClickListener(this); initAccessibility(); } + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + PluginManager.getInstance(getContext()).addPluginListener(RIGHT_BUTTON_PLUGIN, + mRightListener, IntentButtonProvider.VERSION, false /* Only allow one */); + PluginManager.getInstance(getContext()).addPluginListener(LEFT_BUTTON_PLUGIN, + mLeftListener, IntentButtonProvider.VERSION, false /* Only allow one */); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + PluginManager.getInstance(getContext()).removePluginListener(mRightListener); + PluginManager.getInstance(getContext()).removePluginListener(mLeftListener); + } + private void initAccessibility() { mLockIcon.setAccessibilityDelegate(mAccessibilityDelegate); mLeftAffordanceView.setAccessibilityDelegate(mAccessibilityDelegate); - mCameraImageView.setAccessibilityDelegate(mAccessibilityDelegate); + mRightAffordanceView.setAccessibilityDelegate(mAccessibilityDelegate); } @Override @@ -234,11 +263,11 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL getResources().getDimensionPixelSize( com.android.internal.R.dimen.text_size_small_material)); - ViewGroup.LayoutParams lp = mCameraImageView.getLayoutParams(); + ViewGroup.LayoutParams lp = mRightAffordanceView.getLayoutParams(); lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_width); lp.height = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_height); - mCameraImageView.setLayoutParams(lp); - mCameraImageView.setImageDrawable(mContext.getDrawable(R.drawable.ic_camera_alt_24dp)); + mRightAffordanceView.setLayoutParams(lp); + updateRightAffordanceIcon(); lp = mLockIcon.getLayoutParams(); lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_width); @@ -253,6 +282,13 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL updateLeftAffordanceIcon(); } + private void updateRightAffordanceIcon() { + IconState state = mRightButton.getIcon(); + mRightAffordanceView.setVisibility(state.isVisible ? View.VISIBLE : View.GONE); + mRightAffordanceView.setImageDrawable(state.drawable); + mRightAffordanceView.setContentDescription(state.contentDescription); + } + public void setActivityStarter(ActivityStarter activityStarter) { mActivityStarter = activityStarter; } @@ -279,11 +315,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } private Intent getCameraIntent() { - KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext); - boolean canSkipBouncer = updateMonitor.getUserCanSkipBouncer( - KeyguardUpdateMonitor.getCurrentUser()); - boolean secure = mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser()); - return (secure && !canSkipBouncer) ? SECURE_CAMERA_INTENT : INSECURE_CAMERA_INTENT; + return mRightButton.getIntent(); } /** @@ -296,33 +328,19 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } private void updateCameraVisibility() { - if (mCameraImageView == null) { + if (mRightAffordanceView == null) { // Things are not set up yet; reply hazy, ask again later return; } - ResolveInfo resolved = resolveCameraIntent(); - boolean visible = !isCameraDisabledByDpm() && resolved != null - && getResources().getBoolean(R.bool.config_keyguardShowCameraAffordance) - && mUserSetupComplete; - mCameraImageView.setVisibility(visible ? View.VISIBLE : View.GONE); + mRightAffordanceView.setVisibility(mRightButton.getIcon().isVisible + ? View.VISIBLE : View.GONE); } private void updateLeftAffordanceIcon() { - mLeftIsVoiceAssist = canLaunchVoiceAssist(); - int drawableId; - int contentDescription; - boolean visible = mUserSetupComplete; - if (mLeftIsVoiceAssist) { - drawableId = R.drawable.ic_mic_26dp; - contentDescription = R.string.accessibility_voice_assist_button; - } else { - visible &= isPhoneVisible(); - drawableId = R.drawable.ic_phone_24dp; - contentDescription = R.string.accessibility_phone_button; - } - mLeftAffordanceView.setVisibility(visible ? View.VISIBLE : View.GONE); - mLeftAffordanceView.setImageDrawable(mContext.getDrawable(drawableId)); - mLeftAffordanceView.setContentDescription(mContext.getString(contentDescription)); + IconState state = mLeftButton.getIcon(); + mLeftAffordanceView.setVisibility(state.isVisible ? View.VISIBLE : View.GONE); + mLeftAffordanceView.setImageDrawable(state.drawable); + mLeftAffordanceView.setContentDescription(state.contentDescription); } public boolean isLeftVoiceAssist() { @@ -363,16 +381,16 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL @Override public void onStateChanged(boolean accessibilityEnabled, boolean touchExplorationEnabled) { - mCameraImageView.setClickable(touchExplorationEnabled); + mRightAffordanceView.setClickable(touchExplorationEnabled); mLeftAffordanceView.setClickable(touchExplorationEnabled); - mCameraImageView.setFocusable(accessibilityEnabled); + mRightAffordanceView.setFocusable(accessibilityEnabled); mLeftAffordanceView.setFocusable(accessibilityEnabled); mLockIcon.update(); } @Override public void onClick(View v) { - if (v == mCameraImageView) { + if (v == mRightAffordanceView) { launchCamera(CAMERA_LAUNCH_SOURCE_AFFORDANCE); } else if (v == mLeftAffordanceView) { launchLeftAffordance(); @@ -541,7 +559,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } }); } else { - mActivityStarter.startActivity(PHONE_INTENT, false /* dismissShade */); + mActivityStarter.startActivity(mLeftButton.getIntent(), false /* dismissShade */); } } @@ -560,7 +578,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } public KeyguardAffordanceView getRightView() { - return mCameraImageView; + return mRightAffordanceView; } public View getLeftPreview() { @@ -613,7 +631,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL mLeftPreview = mPreviewInflater.inflatePreviewFromService( mAssistManager.getVoiceInteractorComponentName()); } else { - mLeftPreview = mPreviewInflater.inflatePreview(PHONE_INTENT); + mLeftPreview = mPreviewInflater.inflatePreview(mLeftButton.getIntent()); } if (mLeftPreview != null) { mPreviewContainer.addView(mLeftPreview); @@ -629,8 +647,8 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } startFinishDozeAnimationElement(mLockIcon, delay); delay += DOZE_ANIMATION_STAGGER_DELAY; - if (mCameraImageView.getVisibility() == View.VISIBLE) { - startFinishDozeAnimationElement(mCameraImageView, delay); + if (mRightAffordanceView.getVisibility() == View.VISIBLE) { + startFinishDozeAnimationElement(mRightAffordanceView, delay); } mIndicationText.setAlpha(0f); mIndicationText.animate() @@ -664,46 +682,46 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback = new KeyguardUpdateMonitorCallback() { - @Override - public void onUserSwitchComplete(int userId) { - updateCameraVisibility(); - } + @Override + public void onUserSwitchComplete(int userId) { + updateCameraVisibility(); + } - @Override - public void onStartedWakingUp() { - mLockIcon.setDeviceInteractive(true); - } + @Override + public void onStartedWakingUp() { + mLockIcon.setDeviceInteractive(true); + } - @Override - public void onFinishedGoingToSleep(int why) { - mLockIcon.setDeviceInteractive(false); - } + @Override + public void onFinishedGoingToSleep(int why) { + mLockIcon.setDeviceInteractive(false); + } - @Override - public void onScreenTurnedOn() { - mLockIcon.setScreenOn(true); - } + @Override + public void onScreenTurnedOn() { + mLockIcon.setScreenOn(true); + } - @Override - public void onScreenTurnedOff() { - mLockIcon.setScreenOn(false); - } + @Override + public void onScreenTurnedOff() { + mLockIcon.setScreenOn(false); + } - @Override - public void onKeyguardVisibilityChanged(boolean showing) { - mLockIcon.update(); - } + @Override + public void onKeyguardVisibilityChanged(boolean showing) { + mLockIcon.update(); + } - @Override - public void onFingerprintRunningStateChanged(boolean running) { - mLockIcon.update(); - } + @Override + public void onFingerprintRunningStateChanged(boolean running) { + mLockIcon.update(); + } - @Override - public void onStrongAuthStateChanged(int userId) { - mLockIcon.update(); - } - }; + @Override + public void onStrongAuthStateChanged(int userId) { + mLockIcon.update(); + } + }; public void setKeyguardIndicationController( KeyguardIndicationController keyguardIndicationController) { @@ -724,4 +742,96 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL updateLeftAffordance(); inflateCameraPreview(); } + + private void setRightButton(IntentButton button) { + mRightButton = button; + updateRightAffordanceIcon(); + updateCameraVisibility(); + inflateCameraPreview(); + } + + private void setLeftButton(IntentButton button) { + mLeftButton = button; + mLeftIsVoiceAssist = false; + updateLeftAffordance(); + } + + private final PluginListener<IntentButtonProvider> mRightListener = + new PluginListener<IntentButtonProvider>() { + @Override + public void onPluginConnected(IntentButtonProvider plugin) { + setRightButton(plugin.getIntentButton()); + } + + @Override + public void onPluginDisconnected(IntentButtonProvider plugin) { + setRightButton(new DefaultRightButton()); + } + }; + + private final PluginListener<IntentButtonProvider> mLeftListener = + new PluginListener<IntentButtonProvider>() { + @Override + public void onPluginConnected(IntentButtonProvider plugin) { + setLeftButton(plugin.getIntentButton()); + } + + @Override + public void onPluginDisconnected(IntentButtonProvider plugin) { + setLeftButton(new DefaultLeftButton()); + } + }; + + private class DefaultLeftButton implements IntentButton { + + private IconState mIconState = new IconState(); + + @Override + public IconState getIcon() { + mLeftIsVoiceAssist = canLaunchVoiceAssist(); + if (mLeftIsVoiceAssist) { + mIconState.isVisible = mUserSetupComplete; + mIconState.drawable = mContext.getDrawable(R.drawable.ic_mic_26dp); + mIconState.contentDescription = mContext.getString( + R.string.accessibility_voice_assist_button); + } else { + mIconState.isVisible = mUserSetupComplete && isPhoneVisible(); + mIconState.drawable = mContext.getDrawable(R.drawable.ic_phone_24dp); + mIconState.contentDescription = mContext.getString( + R.string.accessibility_phone_button); + } + return mIconState; + } + + @Override + public Intent getIntent() { + return PHONE_INTENT; + } + } + + private class DefaultRightButton implements IntentButton { + + private IconState mIconState = new IconState(); + + @Override + public IconState getIcon() { + ResolveInfo resolved = resolveCameraIntent(); + mIconState.isVisible = !isCameraDisabledByDpm() && resolved != null + && getResources().getBoolean(R.bool.config_keyguardShowCameraAffordance) + && mUserSetupComplete; + mIconState.drawable = mContext.getDrawable(R.drawable.ic_camera_alt_24dp); + mIconState.contentDescription = + mContext.getString(R.string.accessibility_camera_button); + return mIconState; + } + + @Override + public Intent getIntent() { + KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext); + boolean canSkipBouncer = updateMonitor.getUserCanSkipBouncer( + KeyguardUpdateMonitor.getCurrentUser()); + boolean secure = mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser()); + return (secure && !canSkipBouncer) ? SECURE_CAMERA_INTENT : INSECURE_CAMERA_INTENT; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java index 06c8b685ff63..c420927d960b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java @@ -28,12 +28,19 @@ import android.widget.LinearLayout; import android.widget.Space; import com.android.systemui.R; +import com.android.systemui.plugins.PluginListener; +import com.android.systemui.plugins.PluginManager; +import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider; import com.android.systemui.statusbar.policy.KeyButtonView; import com.android.systemui.tuner.TunerService; +import com.android.systemui.tuner.TunerService.Tunable; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; -public class NavigationBarInflaterView extends FrameLayout implements TunerService.Tunable { +public class NavigationBarInflaterView extends FrameLayout + implements Tunable, PluginListener<NavBarButtonProvider> { private static final String TAG = "NavBarInflater"; @@ -57,6 +64,8 @@ public class NavigationBarInflaterView extends FrameLayout implements TunerServi public static final String KEY_IMAGE_DELIM = ":"; public static final String KEY_CODE_END = ")"; + private final List<NavBarButtonProvider> mPlugins = new ArrayList<>(); + protected LayoutInflater mLayoutInflater; protected LayoutInflater mLandscapeInflater; private int mDensity; @@ -129,6 +138,8 @@ public class NavigationBarInflaterView extends FrameLayout implements TunerServi protected void onAttachedToWindow() { super.onAttachedToWindow(); TunerService.get(getContext()).addTunable(this, NAV_BAR_VIEWS); + PluginManager.getInstance(getContext()).addPluginListener(NavBarButtonProvider.ACTION, this, + NavBarButtonProvider.VERSION, true /* Allow multiple */); } @Override @@ -240,8 +251,36 @@ public class NavigationBarInflaterView extends FrameLayout implements TunerServi int indexInParent) { LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater; float size = extractSize(buttonSpec); - String button = extractButton(buttonSpec); + View v = createView(buttonSpec, parent, inflater, landscape); + if (v == null) return null; + + if (size != 0) { + ViewGroup.LayoutParams params = v.getLayoutParams(); + params.width = (int) (params.width * size); + } + parent.addView(v); + addToDispatchers(v, landscape); + View lastView = landscape ? mLastRot90 : mLastRot0; + if (lastView != null) { + v.setAccessibilityTraversalAfter(lastView.getId()); + } + if (landscape) { + mLastRot90 = v; + } else { + mLastRot0 = v; + } + return v; + } + + private View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater, + boolean landscape) { View v = null; + String button = extractButton(buttonSpec); + // Let plugins go first so they can override a standard view if they want. + for (NavBarButtonProvider provider : mPlugins) { + v = provider.createView(buttonSpec, parent); + if (v != null) return v; + } if (HOME.equals(button)) { v = inflater.inflate(R.layout.home, parent, false); if (landscape && isSw600Dp()) { @@ -271,24 +310,6 @@ public class NavigationBarInflaterView extends FrameLayout implements TunerServi if (uri != null) { ((KeyButtonView) v).loadAsync(uri); } - } else { - return null; - } - - if (size != 0) { - ViewGroup.LayoutParams params = v.getLayoutParams(); - params.width = (int) (params.width * size); - } - parent.addView(v); - addToDispatchers(v, landscape); - View lastView = landscape ? mLastRot90 : mLastRot0; - if (lastView != null) { - v.setAccessibilityTraversalAfter(lastView.getId()); - } - if (landscape) { - mLastRot90 = v; - } else { - mLastRot0 = v; } return v; } @@ -374,4 +395,18 @@ public class NavigationBarInflaterView extends FrameLayout implements TunerServi ((ViewGroup) group.getChildAt(i)).removeAllViews(); } } + + @Override + public void onPluginConnected(NavBarButtonProvider plugin) { + mPlugins.add(plugin); + clearViews(); + inflateLayout(mCurrentLayout); + } + + @Override + public void onPluginDisconnected(NavBarButtonProvider plugin) { + mPlugins.remove(plugin); + clearViews(); + inflateLayout(mCurrentLayout); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java index 9e5b881f8a59..3a0eb9409fa4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -60,6 +60,8 @@ public class NavigationBarView extends FrameLayout { // slippery nav bar when everything is disabled, e.g. during setup final static boolean SLIPPERY_WHEN_DISABLED = true; + final static boolean ALTERNATE_CAR_MODE_UI = false; + final Display mDisplay; View mCurrentView = null; View[] mRotatedViews = new View[4]; @@ -95,7 +97,8 @@ public class NavigationBarView extends FrameLayout { private OnVerticalChangedListener mOnVerticalChangedListener; private boolean mLayoutTransitionsEnabled = true; private boolean mWakeAndUnlocking; - private boolean mCarMode = false; + private boolean mUseCarModeUi = false; + private boolean mInCarMode = false; private boolean mDockedStackExists; private final SparseArray<ButtonDispatcher> mButtonDisatchers = new SparseArray<>(); @@ -291,7 +294,9 @@ public class NavigationBarView extends FrameLayout { mMenuIcon = ctx.getDrawable(R.drawable.ic_sysbar_menu); mImeIcon = ctx.getDrawable(R.drawable.ic_ime_switcher_default); - updateCarModeIcons(ctx); + if (ALTERNATE_CAR_MODE_UI) { + updateCarModeIcons(ctx); + } } } @@ -342,14 +347,14 @@ public class NavigationBarView extends FrameLayout { // carmode, respectively. Recents are not available in CarMode in nav bar so change // to recent icon is not required. Drawable backIcon = (backAlt) - ? getBackIconWithAlt(mCarMode, mVertical) - : getBackIcon(mCarMode, mVertical); + ? getBackIconWithAlt(mUseCarModeUi, mVertical) + : getBackIcon(mUseCarModeUi, mVertical); getBackButton().setImageDrawable(backIcon); updateRecentsIcon(); - if (mCarMode) { + if (mUseCarModeUi) { getHomeButton().setImageDrawable(mHomeCarModeIcon); } else { getHomeButton().setImageDrawable(mHomeDefaultIcon); @@ -377,9 +382,9 @@ public class NavigationBarView extends FrameLayout { final boolean disableHome = ((disabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0); - // Disable recents always in car mode. - boolean disableRecent = ( - mCarMode || (disabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0); + // Always disable recents when alternate car mode UI is active. + boolean disableRecent = mUseCarModeUi + || ((disabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0); final boolean disableBack = ((disabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0) && ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) == 0); final boolean disableSearch = ((disabledFlags & View.STATUS_BAR_DISABLE_SEARCH) != 0); @@ -627,14 +632,19 @@ public class NavigationBarView extends FrameLayout { boolean uiCarModeChanged = false; if (newConfig != null) { int uiMode = newConfig.uiMode & Configuration.UI_MODE_TYPE_MASK; - if (mCarMode && uiMode != Configuration.UI_MODE_TYPE_CAR) { - mCarMode = false; - uiCarModeChanged = true; - getHomeButton().setCarMode(mCarMode); - } else if (uiMode == Configuration.UI_MODE_TYPE_CAR) { - mCarMode = true; - uiCarModeChanged = true; - getHomeButton().setCarMode(mCarMode); + final boolean isCarMode = (uiMode == Configuration.UI_MODE_TYPE_CAR); + + if (isCarMode != mInCarMode) { + mInCarMode = isCarMode; + getHomeButton().setCarMode(isCarMode); + + if (ALTERNATE_CAR_MODE_UI) { + mUseCarModeUi = isCarMode; + uiCarModeChanged = true; + } else { + // Don't use car mode behavior if ALTERNATE_CAR_MODE_UI not set. + mUseCarModeUi = false; + } } } return uiCarModeChanged; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java index 5d1af2f5591c..fb2c335f96bb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java @@ -328,7 +328,7 @@ public class NotificationPanelView extends PanelView implements } else if (!mQsExpanded) { setQsExpansion(mQsMinExpansionHeight + mLastOverscroll); } - updateStackHeight(getExpandedHeight()); + updateExpandedHeight(getExpandedHeight()); updateHeader(); // If we are running a size change animation, the animation takes care of the height of @@ -376,10 +376,7 @@ public class NotificationPanelView extends PanelView implements boolean animate = mNotificationStackScroller.isAddOrRemoveAnimationPending(); int stackScrollerPadding; if (mStatusBarState != StatusBarState.KEYGUARD) { - int bottom = mQsContainer.getHeader().getHeight(); - stackScrollerPadding = mStatusBarState == StatusBarState.SHADE - ? bottom + mQsPeekHeight - : mKeyguardStatusBar.getHeight(); + stackScrollerPadding = mQsContainer.getHeader().getHeight() + mQsPeekHeight; mTopPaddingAdjustment = 0; } else { mClockPositionAlgorithm.setup( @@ -1166,6 +1163,7 @@ public class NotificationPanelView extends PanelView implements private void updateQsState() { mQsContainer.setExpanded(mQsExpanded); + mNotificationStackScroller.setQsExpanded(mQsExpanded); mNotificationStackScroller.setScrollingEnabled( mStatusBarState != StatusBarState.KEYGUARD && (!mQsExpanded || mQsExpansionFromOverscroll)); @@ -1427,7 +1425,7 @@ public class NotificationPanelView extends PanelView implements setQsExpansion(mQsMinExpansionHeight + t * (getTempQsMaxExpansion() - mQsMinExpansionHeight)); } - updateStackHeight(expandedHeight); + updateExpandedHeight(expandedHeight); updateHeader(); updateUnlockIcon(); updateNotificationTranslucency(); @@ -1487,7 +1485,7 @@ public class NotificationPanelView extends PanelView implements maxQsHeight, mStatusBarState == StatusBarState.KEYGUARD ? mClockPositionResult.stackScrollerPadding - mTopPaddingAdjustment : 0) - + notificationHeight; + + notificationHeight + mNotificationStackScroller.getTopPaddingOverflow(); if (totalHeight > mNotificationStackScroller.getHeight()) { float fullyCollapsedHeight = maxQsHeight + mNotificationStackScroller.getLayoutMinHeight(); @@ -1730,6 +1728,14 @@ public class NotificationPanelView extends PanelView implements if (view == null && mQsExpanded) { return; } + ExpandableView firstChildNotGone = mNotificationStackScroller.getFirstChildNotGone(); + ExpandableNotificationRow firstRow = firstChildNotGone instanceof ExpandableNotificationRow + ? (ExpandableNotificationRow) firstChildNotGone + : null; + if (firstRow != null + && (view == firstRow || (firstRow.getNotificationParent() == firstRow))) { + requestScrollerTopPaddingUpdate(false); + } requestPanelHeightUpdate(); } @@ -2249,8 +2255,8 @@ public class NotificationPanelView extends PanelView implements mQsAutoReinflateContainer.setTranslationX(translation); } - protected void updateStackHeight(float stackHeight) { - mNotificationStackScroller.setStackHeight(stackHeight); + protected void updateExpandedHeight(float expandedHeight) { + mNotificationStackScroller.setExpandedHeight(expandedHeight); updateKeyguardBottomAreaAlpha(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java index 40303c4ac37c..8ee014c4583a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -313,7 +313,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, PhoneStatusBarPolicy mIconPolicy; // These are no longer handled by the policy, because we need custom strategies for them - BluetoothControllerImpl mBluetoothController; + protected BluetoothControllerImpl mBluetoothController; SecurityControllerImpl mSecurityController; protected BatteryController mBatteryController; LocationControllerImpl mLocationController; @@ -1733,7 +1733,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, protected void performRemoveNotification(StatusBarNotification n, boolean removeView) { Entry entry = mNotificationData.get(n.getKey()); if (mRemoteInputController.isRemoteInputActive(entry)) { - mRemoteInputController.removeRemoteInput(entry); + mRemoteInputController.removeRemoteInput(entry, null); } super.performRemoveNotification(n, removeView); } @@ -2681,7 +2681,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, private void removeRemoteInputEntriesKeptUntilCollapsed() { for (int i = 0; i < mRemoteInputEntriesToRemoveOnCollapse.size(); i++) { Entry entry = mRemoteInputEntriesToRemoveOnCollapse.valueAt(i); - mRemoteInputController.removeRemoteInput(entry); + mRemoteInputController.removeRemoteInput(entry, null); removeNotification(entry.key, mLatestRankingMap); } mRemoteInputEntriesToRemoveOnCollapse.clear(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java index b9c7a4b411ef..6726c9200a58 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java @@ -53,6 +53,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC protected boolean mCharged; protected boolean mPowerSave; private boolean mTestmode = false; + private boolean mHasReceivedBattery = false; public BatteryControllerImpl(Context context) { mContext = context; @@ -92,6 +93,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC synchronized (mChangeCallbacks) { mChangeCallbacks.add(cb); } + if (!mHasReceivedBattery) return; cb.onBatteryLevelChanged(mLevel, mPluggedIn, mCharging); cb.onPowerSaveChanged(mPowerSave); } @@ -108,6 +110,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC final String action = intent.getAction(); if (action.equals(Intent.ACTION_BATTERY_CHANGED)) { if (mTestmode && !intent.getBooleanExtra("testmode", false)) return; + mHasReceivedBattery = true; mLevel = (int)(100f * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java index e6066aaa4c31..bcc5a3fa0a1b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java @@ -44,12 +44,12 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ImageView; import com.android.systemui.R; -import com.android.systemui.statusbar.phone.ButtonDispatcher; +import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider.ButtonInterface; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK; -public class KeyButtonView extends ImageView implements ButtonDispatcher.ButtonInterface { +public class KeyButtonView extends ImageView implements ButtonInterface { private int mContentDescriptionRes; private long mDownTime; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index ab2a8bc6e92b..7b1f7071b5c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -69,6 +69,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene // A marker object that let's us easily find views of this class. public static final Object VIEW_TAG = new Object(); + public final Object mToken = new Object(); + private RemoteEditText mEditText; private ImageButton mSendButton; private ProgressBar mProgressBar; @@ -140,8 +142,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mSendButton.setVisibility(INVISIBLE); mProgressBar.setVisibility(VISIBLE); mEntry.remoteInputText = mEditText.getText(); - mController.addSpinning(mEntry.key); - mController.removeRemoteInput(mEntry); + mController.addSpinning(mEntry.key, mToken); + mController.removeRemoteInput(mEntry, mToken); mEditText.mShowImeOnInputConnection = false; mController.remoteInputSent(mEntry); @@ -193,7 +195,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } private void onDefocus(boolean animate) { - mController.removeRemoteInput(mEntry); + mController.removeRemoteInput(mEntry, mToken); mEntry.remoteInputText = mEditText.getText(); // During removal, we get reattached and lose focus. Not hiding in that @@ -232,11 +234,11 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - if (mEntry.row.isChangingPosition()) { + if (mEntry.row.isChangingPosition() || isTemporarilyDetached()) { return; } - mController.removeRemoteInput(mEntry); - mController.removeSpinning(mEntry.key); + mController.removeRemoteInput(mEntry, mToken); + mController.removeSpinning(mEntry.key, mToken); } public void setPendingIntent(PendingIntent pendingIntent) { @@ -265,7 +267,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mEntry.notification.getPackageName()); setVisibility(VISIBLE); - mController.addRemoteInput(mEntry); + mController.addRemoteInput(mEntry, mToken); mEditText.setInnerFocusable(true); mEditText.mShowImeOnInputConnection = true; mEditText.setText(mEntry.remoteInputText); @@ -290,7 +292,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mEditText.setEnabled(true); mSendButton.setVisibility(VISIBLE); mProgressBar.setVisibility(INVISIBLE); - mController.removeSpinning(mEntry.key); + mController.removeSpinning(mEntry.key, mToken); updateSendButton(); onDefocus(false /* animate */); @@ -432,6 +434,24 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mRevealR = r; } + @Override + public void dispatchStartTemporaryDetach() { + super.dispatchStartTemporaryDetach(); + // Detach the EditText temporarily such that it doesn't get onDetachedFromWindow and + // won't lose IME focus. + detachViewFromParent(mEditText); + } + + @Override + public void dispatchFinishTemporaryDetach() { + if (isAttachedToWindow()) { + attachViewToParent(mEditText, 0, mEditText.getLayoutParams()); + } else { + removeDetachedView(mEditText, false /* animate */); + } + super.dispatchFinishTemporaryDetach(); + } + /** * An EditText that changes appearance based on whether it's focusable and becomes * un-focusable whenever the user navigates away from it or it becomes invisible. @@ -448,7 +468,15 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } private void defocusIfNeeded(boolean animate) { - if (mRemoteInputView != null && mRemoteInputView.mEntry.row.isChangingPosition()) { + if (mRemoteInputView != null && mRemoteInputView.mEntry.row.isChangingPosition() + || isTemporarilyDetached()) { + if (isTemporarilyDetached()) { + // We might get reattached but then the other one of HUN / expanded might steal + // our focus, so we'll need to save our text here. + if (mRemoteInputView != null) { + mRemoteInputView.mEntry.remoteInputText = getText(); + } + } return; } if (isFocusable() && isEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java index 50e5b886488a..81da6720828a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java @@ -43,6 +43,7 @@ public class AmbientState { private boolean mShadeExpanded; private float mMaxHeadsUpTranslation; private boolean mDismissAllInProgress; + private int mLayoutMinHeight; public int getScrollY() { return mScrollY; @@ -137,10 +138,6 @@ public class AmbientState { mStackTranslation = stackTranslation; } - public int getLayoutHeight() { - return mLayoutHeight; - } - public void setLayoutHeight(int layoutHeight) { mLayoutHeight = layoutHeight; } @@ -154,7 +151,7 @@ public class AmbientState { } public int getInnerHeight() { - return mLayoutHeight - mTopPadding; + return Math.max(mLayoutHeight - mTopPadding, mLayoutMinHeight); } public boolean isShadeExpanded() { @@ -180,4 +177,8 @@ public class AmbientState { public boolean isDismissAllInProgress() { return mDismissAllInProgress; } + + public void setLayoutMinHeight(int layoutMinHeight) { + mLayoutMinHeight = layoutMinHeight; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java index 90f410079fbb..9dc9062aeb8e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java @@ -112,11 +112,7 @@ public class NotificationStackScrollLayout extends ViewGroup private int mCurrentStackHeight = Integer.MAX_VALUE; private final Paint mBackgroundPaint = new Paint(); - /** - * mCurrentStackHeight is the actual stack height, mLastSetStackHeight is the stack height set - * externally from {@link #setStackHeight} - */ - private float mLastSetStackHeight; + private float mExpandedHeight; private int mOwnScrollY; private int mMaxLayoutHeight; @@ -355,6 +351,9 @@ public class NotificationStackScrollLayout extends ViewGroup return object.getBackgroundFadeAmount(); } }; + private boolean mQsExpanded; + private boolean mForwardScrollable; + private boolean mBackwardScrollable; public NotificationStackScrollLayout(Context context) { this(context, null); @@ -520,6 +519,7 @@ public class NotificationStackScrollLayout extends ViewGroup clampScrollPosition(); requestChildrenUpdate(); updateFirstAndLastBackgroundViews(); + updateAlgorithmLayoutMinHeight(); } private void requestAnimationOnViewResize(ExpandableNotificationRow row) { @@ -561,9 +561,14 @@ public class NotificationStackScrollLayout extends ViewGroup private void updateAlgorithmHeightAndPadding() { mAmbientState.setLayoutHeight(getLayoutHeight()); + updateAlgorithmLayoutMinHeight(); mAmbientState.setTopPadding(mTopPadding); } + private void updateAlgorithmLayoutMinHeight() { + mAmbientState.setLayoutMinHeight(mQsExpanded && !onKeyguard() ? getLayoutMinHeight() : 0); + } + /** * Updates the children views according to the stack scroll algorithm. Call this whenever * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout. @@ -594,7 +599,7 @@ public class NotificationStackScrollLayout extends ViewGroup if (startingPosition < mOwnScrollY) { // This child starts off screen, so let's keep it offscreen to keep the others visible - mOwnScrollY += childHeight; + setOwnScrollY(mOwnScrollY + childHeight); } } } @@ -617,7 +622,7 @@ public class NotificationStackScrollLayout extends ViewGroup // Only apply the scroll if we're scrolling the view upwards, or the view is so far up // that it is not visible anymore. if (mOwnScrollY < targetScroll || outOfViewScroll < mOwnScrollY) { - mOwnScrollY = targetScroll; + setOwnScrollY(targetScroll); } } } @@ -637,7 +642,7 @@ public class NotificationStackScrollLayout extends ViewGroup private void clampScrollPosition() { int scrollRange = getScrollRange(); if (scrollRange < mOwnScrollY) { - mOwnScrollY = scrollRange; + setOwnScrollY(scrollRange); } } @@ -660,19 +665,19 @@ public class NotificationStackScrollLayout extends ViewGroup } /** - * Update the height of the stack to a new height. + * Update the height of the panel. * - * @param height the new height of the stack + * @param height the expanded height of the panel */ - public void setStackHeight(float height) { - mLastSetStackHeight = height; + public void setExpandedHeight(float height) { + mExpandedHeight = height; setIsExpanded(height > 0.0f); int stackHeight; float translationY; float appearEndPosition = getAppearEndPosition(); float appearStartPosition = getAppearStartPosition(); if (height >= appearEndPosition) { - translationY = mTopPaddingOverflow; + translationY = 0; stackHeight = (int) height; } else { float appearFraction = getAppearFraction(height); @@ -699,8 +704,12 @@ public class NotificationStackScrollLayout extends ViewGroup * Measured relative to the resting position. */ private float getExpandTranslationStart() { - int startPosition = mTrackingHeadsUp || mHeadsUpManager.hasPinnedHeadsUp() - ? 0 : -getFirstChildIntrinsicHeight(); + int startPosition = 0; + if (!mTrackingHeadsUp && !mHeadsUpManager.hasPinnedHeadsUp()) { + startPosition = - Math.min(getFirstChildIntrinsicHeight(), + mMaxLayoutHeight - mIntrinsicPadding - mBottomStackSlowDownHeight + - mBottomStackPeekSize); + } return startPosition - mTopPadding; } @@ -723,7 +732,7 @@ public class NotificationStackScrollLayout extends ViewGroup ? mHeadsUpManager.getTopHeadsUpPinnedHeight() + mBottomStackPeekSize + mBottomStackSlowDownHeight : getLayoutMinHeight(); - return firstItemHeight + mTopPadding + mTopPaddingOverflow; + return firstItemHeight + (onKeyguard() ? mTopPadding : mIntrinsicPadding); } /** @@ -1153,6 +1162,10 @@ public class NotificationStackScrollLayout extends ViewGroup @Override public boolean isAntiFalsingNeeded() { + return onKeyguard(); + } + + private boolean onKeyguard() { return mPhoneStatusBar.getBarState() == StatusBarState.KEYGUARD; } @@ -1262,7 +1275,7 @@ public class NotificationStackScrollLayout extends ViewGroup if (!isScrollingEnabled()) { return false; } - if (isInsideQsContainer(ev)) { + if (isInsideQsContainer(ev) && !mIsBeingDragged) { return false; } mForcedScroll = null; @@ -1431,7 +1444,7 @@ public class NotificationStackScrollLayout extends ViewGroup false /* onTop */, false /* animate */); } - mOwnScrollY = range; + setOwnScrollY(range); scrollAmount = 0.0f; } return scrollAmount; @@ -1462,7 +1475,7 @@ public class NotificationStackScrollLayout extends ViewGroup setOverScrolledPixels(currentTopPixels - newScrollY, true /* onTop */, false /* animate */); - mOwnScrollY = 0; + setOwnScrollY(0); scrollAmount = 0.0f; } return scrollAmount; @@ -1676,7 +1689,7 @@ public class NotificationStackScrollLayout extends ViewGroup } private void customScrollTo(int y) { - mOwnScrollY = y; + setOwnScrollY(y); updateChildren(); } @@ -1687,7 +1700,7 @@ public class NotificationStackScrollLayout extends ViewGroup final int oldX = mScrollX; final int oldY = mOwnScrollY; mScrollX = scrollX; - mOwnScrollY = scrollY; + setOwnScrollY(scrollY); if (clampedY) { springBack(); } else { @@ -1717,12 +1730,12 @@ public class NotificationStackScrollLayout extends ViewGroup if (overScrolledTop) { onTop = true; newAmount = -mOwnScrollY; - mOwnScrollY = 0; + setOwnScrollY(0); mDontReportNextOverScroll = true; } else { onTop = false; newAmount = mOwnScrollY - scrollRange; - mOwnScrollY = scrollRange; + setOwnScrollY(scrollRange); } setOverScrollAmount(newAmount, onTop, false); setOverScrollAmount(0.0f, onTop, true); @@ -1850,6 +1863,19 @@ public class NotificationStackScrollLayout extends ViewGroup if (scrollable != mScrollable) { mScrollable = scrollable; setFocusable(scrollable); + updateForwardAndBackwardScrollability(); + } + } + + private void updateForwardAndBackwardScrollability() { + boolean forwardScrollable = mScrollable && mOwnScrollY < getScrollRange(); + boolean backwardsScrollable = mScrollable && mOwnScrollY > 0; + boolean changed = forwardScrollable != mForwardScrollable + || backwardsScrollable != mBackwardScrollable; + mForwardScrollable = forwardScrollable; + mBackwardScrollable = backwardsScrollable; + if (changed) { + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } } @@ -2109,13 +2135,13 @@ public class NotificationStackScrollLayout extends ViewGroup float topAmount = getCurrentOverScrollAmount(true); float bottomAmount = getCurrentOverScrollAmount(false); if (velocityY < 0 && topAmount > 0) { - mOwnScrollY -= (int) topAmount; + setOwnScrollY(mOwnScrollY - (int) topAmount); mDontReportNextOverScroll = true; setOverScrollAmount(0, true, false); mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor(true /* onTop */) * mOverflingDistance + topAmount; } else if (velocityY > 0 && bottomAmount > 0) { - mOwnScrollY += bottomAmount; + setOwnScrollY((int) (mOwnScrollY + bottomAmount)); setOverScrollAmount(0, false, false); mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor(false /* onTop */) * mOverflingDistance @@ -2158,26 +2184,22 @@ public class NotificationStackScrollLayout extends ViewGroup */ public void updateTopPadding(float qsHeight, boolean animate, boolean ignoreIntrinsicPadding) { - float start = qsHeight; - float stackHeight = getHeight() - start; + int topPadding = (int) qsHeight; int minStackHeight = getLayoutMinHeight(); - if (stackHeight <= minStackHeight) { - float overflow = minStackHeight - stackHeight; - stackHeight = minStackHeight; - start = getHeight() - stackHeight; - mTopPaddingOverflow = overflow; + if (topPadding + minStackHeight > getHeight()) { + mTopPaddingOverflow = topPadding + minStackHeight - getHeight(); } else { mTopPaddingOverflow = 0; } - setTopPadding(ignoreIntrinsicPadding ? (int) start : clampPadding((int) start), + setTopPadding(ignoreIntrinsicPadding ? topPadding : clampPadding(topPadding), animate); - setStackHeight(mLastSetStackHeight); + setExpandedHeight(mExpandedHeight); } public int getLayoutMinHeight() { int firstChildMinHeight = getFirstChildIntrinsicHeight(); return Math.min(firstChildMinHeight + mBottomStackPeekSize + mBottomStackSlowDownHeight, - mMaxLayoutHeight - mTopPadding); + mMaxLayoutHeight - mIntrinsicPadding); } public int getFirstChildIntrinsicHeight() { @@ -2470,11 +2492,11 @@ public class NotificationStackScrollLayout extends ViewGroup if (endPosition <= mOwnScrollY) { // This child is fully scrolled of the top, so we have to deduct its height from the // scrollPosition - mOwnScrollY -= childHeight; + setOwnScrollY(mOwnScrollY - childHeight); } else if (startingPosition < mOwnScrollY) { // This child is currently being scrolled into, set the scroll position to the start of // this child - mOwnScrollY = startingPosition; + setOwnScrollY(startingPosition); } } @@ -3059,7 +3081,7 @@ public class NotificationStackScrollLayout extends ViewGroup public void onExpansionStopped() { mIsExpansionChanging = false; if (!mIsExpanded) { - mOwnScrollY = 0; + setOwnScrollY(0); mPhoneStatusBar.resetUserExpandedStates(); // lets make sure nothing is in the overlay / transient anymore @@ -3092,7 +3114,7 @@ public class NotificationStackScrollLayout extends ViewGroup public void resetScrollPosition() { mScroller.abortAnimation(); - mOwnScrollY = 0; + setOwnScrollY(0); } private void setIsExpanded(boolean isExpanded) { @@ -3128,10 +3150,14 @@ public class NotificationStackScrollLayout extends ViewGroup updateScrollPositionOnExpandInBottom(view); clampScrollPosition(); notifyHeightChangeListener(view); + ExpandableNotificationRow row = view instanceof ExpandableNotificationRow + ? (ExpandableNotificationRow) view + : null; + if (row != null && (row == mFirstVisibleBackgroundChild + || row.getNotificationParent() == mFirstVisibleBackgroundChild)) { + updateAlgorithmLayoutMinHeight(); + } if (needsAnimation) { - ExpandableNotificationRow row = view instanceof ExpandableNotificationRow - ? (ExpandableNotificationRow) view - : null; requestAnimationOnViewResize(row); } requestChildrenUpdate(); @@ -3157,7 +3183,7 @@ public class NotificationStackScrollLayout extends ViewGroup } int stackEnd = getStackEndPosition(); if (endPosition > stackEnd) { - mOwnScrollY += endPosition - stackEnd; + setOwnScrollY((int) (mOwnScrollY + endPosition - stackEnd)); mDisallowScrollingInThisMotion = true; } } @@ -3414,7 +3440,7 @@ public class NotificationStackScrollLayout extends ViewGroup } private int findDarkAnimationOriginIndex(@Nullable PointF screenLocation) { - if (screenLocation == null || screenLocation.y < mTopPadding + mTopPaddingOverflow) { + if (screenLocation == null || screenLocation.y < mTopPadding) { return AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_ABOVE; } if (screenLocation.y > getBottomMostNotificationBottom()) { @@ -3719,15 +3745,14 @@ public class NotificationStackScrollLayout extends ViewGroup @Override public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfoInternal(info); - final int scrollRange = getScrollRange(); - if (scrollRange > 0) { + if (mScrollable) { info.setScrollable(true); - if (mScrollY > 0) { + if (mBackwardScrollable) { info.addAction( AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP); } - if (mScrollY < scrollRange) { + if (mForwardScrollable) { info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN); } @@ -3898,6 +3923,18 @@ public class NotificationStackScrollLayout extends ViewGroup mCurrentStackScrollState.removeViewStateForView(view); } + public void setQsExpanded(boolean qsExpanded) { + mQsExpanded = qsExpanded; + updateAlgorithmLayoutMinHeight(); + } + + public void setOwnScrollY(int ownScrollY) { + if (ownScrollY != mOwnScrollY) { + mOwnScrollY = ownScrollY; + updateForwardAndBackwardScrollability(); + } + } + /** * A listener that is notified when some child locations might have changed. */ @@ -4121,7 +4158,7 @@ public class NotificationStackScrollLayout extends ViewGroup onDragCancelled(animView); // If we're on the lockscreen we want to false this. - if (mPhoneStatusBar.getBarState() == StatusBarState.KEYGUARD) { + if (isAntiFalsingNeeded()) { mHandler.removeCallbacks(mFalsingCheck); mHandler.postDelayed(mFalsingCheck, COVER_GEAR_DELAY); } diff --git a/packages/SystemUI/src/com/android/systemui/tuner/PluginFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/PluginFragment.java new file mode 100644 index 000000000000..132a6dd6c747 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/tuner/PluginFragment.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.tuner; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.support.v14.preference.PreferenceFragment; +import android.support.v14.preference.SwitchPreference; +import android.support.v7.preference.PreferenceCategory; +import android.support.v7.preference.PreferenceScreen; +import android.support.v7.preference.PreferenceViewHolder; +import android.view.View; + +import com.android.systemui.plugins.PluginPrefs; +import com.android.systemui.R; + +import java.util.List; +import java.util.Set; + +public class PluginFragment extends PreferenceFragment { + + public static final String ACTION_PLUGIN_SETTINGS + = "com.android.systemui.action.PLUGIN_SETTINGS"; + + private PluginPrefs mPluginPrefs; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(getContext()); + screen.setOrderingAsAdded(false); + Context prefContext = getPreferenceManager().getContext(); + mPluginPrefs = new PluginPrefs(getContext()); + Set<String> pluginActions = mPluginPrefs.getPluginList(); + for (String action : pluginActions) { + String name = action.replace("com.android.systemui.action.PLUGIN_", ""); + PreferenceCategory category = new PreferenceCategory(prefContext); + category.setTitle(name); + + List<ResolveInfo> result = getContext().getPackageManager().queryIntentServices( + new Intent(action), PackageManager.MATCH_DISABLED_COMPONENTS); + if (result.size() > 0) { + screen.addPreference(category); + } + for (ResolveInfo info : result) { + category.addPreference(new PluginPreference(prefContext, info)); + } + } + setPreferenceScreen(screen); + } + + private static class PluginPreference extends SwitchPreference { + private final ComponentName mComponent; + private final boolean mHasSettings; + + public PluginPreference(Context prefContext, ResolveInfo info) { + super(prefContext); + mComponent = new ComponentName(info.serviceInfo.packageName, info.serviceInfo.name); + PackageManager pm = prefContext.getPackageManager(); + mHasSettings = pm.resolveActivity(new Intent(ACTION_PLUGIN_SETTINGS) + .setPackage(mComponent.getPackageName()), 0) != null; + setTitle(info.serviceInfo.loadLabel(pm)); + setChecked(pm.getComponentEnabledSetting(mComponent) + != PackageManager.COMPONENT_ENABLED_STATE_DISABLED); + setWidgetLayoutResource(R.layout.tuner_widget_settings_switch); + } + + @Override + protected boolean persistBoolean(boolean value) { + getContext().getPackageManager().setComponentEnabledSetting(mComponent, + value ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + return true; + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + holder.findViewById(R.id.settings).setVisibility(mHasSettings ? View.VISIBLE + : View.GONE); + holder.findViewById(R.id.divider).setVisibility(mHasSettings ? View.VISIBLE + : View.GONE); + holder.findViewById(R.id.settings).setOnClickListener(v -> { + ResolveInfo result = v.getContext().getPackageManager().resolveActivity( + new Intent(ACTION_PLUGIN_SETTINGS).setPackage( + mComponent.getPackageName()), 0); + if (result != null) { + v.getContext().startActivity(new Intent().setComponent( + new ComponentName(result.activityInfo.packageName, + result.activityInfo.name))); + } + }); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java index 70f2fdcfa8d3..7f63418de324 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java @@ -19,16 +19,9 @@ import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; import android.content.DialogInterface; -import android.database.ContentObserver; -import android.net.Uri; import android.os.Bundle; -import android.os.Handler; import android.provider.Settings; -import android.provider.Settings.System; import android.support.v14.preference.PreferenceFragment; -import android.support.v14.preference.SwitchPreference; -import android.support.v7.preference.Preference; -import android.support.v7.preference.Preference.OnPreferenceChangeListener; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -36,12 +29,14 @@ import android.view.MenuItem; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.MetricsProto.MetricsEvent; import com.android.systemui.R; +import com.android.systemui.plugins.PluginPrefs; public class TunerFragment extends PreferenceFragment { private static final String TAG = "TunerFragment"; private static final String KEY_BATTERY_PCT = "battery_pct"; + private static final String KEY_PLUGINS = "plugins"; public static final String SETTING_SEEN_TUNER_WARNING = "seen_tuner_warning"; @@ -65,6 +60,9 @@ public class TunerFragment extends PreferenceFragment { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.tuner_prefs); + if (!PluginPrefs.hasPlugins(getContext())) { + getPreferenceScreen().removePreference(findPreference(KEY_PLUGINS)); + } if (Settings.Secure.getInt(getContext().getContentResolver(), SETTING_SEEN_TUNER_WARNING, 0) == 0) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconListTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconListTest.java new file mode 100644 index 000000000000..2792d8c45865 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconListTest.java @@ -0,0 +1,85 @@ +package com.android.systemui.statusbar; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNull; + +import static org.mockito.Mockito.mock; + +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.internal.statusbar.StatusBarIcon; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.phone.StatusBarIconList; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class StatusBarIconListTest extends SysuiTestCase { + + private final static String[] STATUS_BAR_SLOTS = {"aaa", "bbb", "ccc"}; + + @Test + public void testGetExistingSlot() { + StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS); + assertEquals(1, statusBarIconList.getSlotIndex("bbb")); + assertEquals(2, statusBarIconList.getSlotIndex("ccc")); + } + + @Test + public void testGetNonexistingSlot() { + StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS); + assertEquals(0, statusBarIconList.getSlotIndex("aaa")); + assertEquals(3, statusBarIconList.size()); + assertEquals(0, statusBarIconList.getSlotIndex("zzz")); // new content added in front + assertEquals(1, statusBarIconList.getSlotIndex("aaa")); // slid back + assertEquals(4, statusBarIconList.size()); + } + + @Test + public void testAddSlotSlidesIcons() { + StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS); + StatusBarIcon sbIcon = mock(StatusBarIcon.class); + statusBarIconList.setIcon(0, sbIcon); + statusBarIconList.getSlotIndex("zzz"); // new content added in front + assertNull(statusBarIconList.getIcon(0)); + assertEquals(sbIcon, statusBarIconList.getIcon(1)); + } + + @Test + public void testGetAndSetIcon() { + StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS); + StatusBarIcon sbIconA = mock(StatusBarIcon.class); + StatusBarIcon sbIconB = mock(StatusBarIcon.class); + statusBarIconList.setIcon(0, sbIconA); + statusBarIconList.setIcon(1, sbIconB); + assertEquals(sbIconA, statusBarIconList.getIcon(0)); + assertEquals(sbIconB, statusBarIconList.getIcon(1)); + assertNull(statusBarIconList.getIcon(2)); // icon not set + } + + @Test + public void testRemoveIcon() { + StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS); + StatusBarIcon sbIconA = mock(StatusBarIcon.class); + StatusBarIcon sbIconB = mock(StatusBarIcon.class); + statusBarIconList.setIcon(0, sbIconA); + statusBarIconList.setIcon(1, sbIconB); + statusBarIconList.removeIcon(0); + assertNull(statusBarIconList.getIcon(0)); // icon not set + } + + @Test + public void testGetViewIndex() { + StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS); + StatusBarIcon sbIcon = mock(StatusBarIcon.class); + statusBarIconList.setIcon(2, sbIcon); + assertEquals(0, statusBarIconList.getViewIndex(2)); // Icon for item 2 is 0th child view. + statusBarIconList.setIcon(0, sbIcon); + assertEquals(0, statusBarIconList.getViewIndex(0)); // Icon for item 0 is 0th child view, + assertEquals(1, statusBarIconList.getViewIndex(2)); // and item 2 is now 1st child view. + } + +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/AppOpsService.java b/services/core/java/com/android/server/AppOpsService.java index 4caeba84b202..7c2eea3f68aa 100644 --- a/services/core/java/com/android/server/AppOpsService.java +++ b/services/core/java/com/android/server/AppOpsService.java @@ -51,6 +51,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; +import android.os.ShellCallback; import android.os.ShellCommand; import android.os.UserHandle; import android.os.storage.MountServiceInternal; @@ -1787,8 +1788,9 @@ public class AppOpsService extends IAppOpsService.Stub { } @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, - FileDescriptor err, String[] args, ResultReceiver resultReceiver) { - (new Shell(this, this)).exec(this, in, out, err, args, resultReceiver); + FileDescriptor err, String[] args, ShellCallback callback, + ResultReceiver resultReceiver) { + (new Shell(this, this)).exec(this, in, out, err, args, callback, resultReceiver); } static void dumpCommandHelp(PrintWriter pw) { @@ -2277,6 +2279,16 @@ public class AppOpsService extends IAppOpsService.Stub { ClientRestrictionState opRestrictions = mOpUserRestrictions.valueAt(i); opRestrictions.removeUser(userHandle); } + removeUidsForUserLocked(userHandle); + } + } + + private void removeUidsForUserLocked(int userHandle) { + for (int i = mUidStates.size() - 1; i >= 0; --i) { + final int uid = mUidStates.keyAt(i); + if (UserHandle.getUserId(uid) == userHandle) { + mUidStates.removeAt(i); + } } } @@ -2396,6 +2408,12 @@ public class AppOpsService extends IAppOpsService.Stub { perUserExcludedPackages = null; } } + if (perUserRestrictions != null) { + perUserRestrictions.remove(userId); + if (perUserRestrictions.size() <= 0) { + perUserRestrictions = null; + } + } } public boolean isDefault() { diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java index 6b517210ef31..d2cfb6d51002 100644 --- a/services/core/java/com/android/server/BatteryService.java +++ b/services/core/java/com/android/server/BatteryService.java @@ -20,6 +20,7 @@ import android.database.ContentObserver; import android.os.BatteryStats; import android.os.ResultReceiver; +import android.os.ShellCallback; import android.os.ShellCommand; import com.android.internal.app.IBatteryStats; import com.android.server.am.BatteryStatsService; @@ -789,7 +790,7 @@ public final class BatteryService extends SystemService { pw.println(" technology: " + mBatteryProps.batteryTechnology); } else { Shell shell = new Shell(); - shell.exec(mBinderService, null, fd, null, args, new ResultReceiver(null)); + shell.exec(mBinderService, null, fd, null, args, null, new ResultReceiver(null)); } } } @@ -875,8 +876,9 @@ public final class BatteryService extends SystemService { } @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, - FileDescriptor err, String[] args, ResultReceiver resultReceiver) { - (new Shell()).exec(this, in, out, err, args, resultReceiver); + FileDescriptor err, String[] args, ShellCallback callback, + ResultReceiver resultReceiver) { + (new Shell()).exec(this, in, out, err, args, callback, resultReceiver); } } diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index a2f3be720530..fa2f6ac3af82 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -5502,7 +5502,7 @@ public class ConnectivityService extends IConnectivityManager.Stub @Override public String getCaptivePortalServerUrl() { - return NetworkMonitor.getCaptivePortalServerUrl(mContext); + return NetworkMonitor.getCaptivePortalServerHttpUrl(mContext); } @Override diff --git a/services/core/java/com/android/server/DeviceIdleController.java b/services/core/java/com/android/server/DeviceIdleController.java index 6b73fec8c920..dbc1f31a9348 100644 --- a/services/core/java/com/android/server/DeviceIdleController.java +++ b/services/core/java/com/android/server/DeviceIdleController.java @@ -60,6 +60,7 @@ import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; +import android.os.ShellCallback; import android.os.ShellCommand; import android.os.SystemClock; import android.os.UserHandle; @@ -1232,8 +1233,8 @@ public class DeviceIdleController extends SystemService } @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, - FileDescriptor err, String[] args, ResultReceiver resultReceiver) { - (new Shell()).exec(this, in, out, err, args, resultReceiver); + FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver) { + (new Shell()).exec(this, in, out, err, args, callback, resultReceiver); } } @@ -2838,7 +2839,8 @@ public class DeviceIdleController extends SystemService shell.userId = userId; String[] newArgs = new String[args.length-i]; System.arraycopy(args, i, newArgs, 0, args.length-i); - shell.exec(mBinderService, null, fd, null, newArgs, new ResultReceiver(null)); + shell.exec(mBinderService, null, fd, null, newArgs, null, + new ResultReceiver(null)); return; } } diff --git a/services/core/java/com/android/server/RecoverySystemService.java b/services/core/java/com/android/server/RecoverySystemService.java index 276687f844da..3c8c699a65bb 100644 --- a/services/core/java/com/android/server/RecoverySystemService.java +++ b/services/core/java/com/android/server/RecoverySystemService.java @@ -21,6 +21,7 @@ import android.net.LocalSocket; import android.net.LocalSocketAddress; import android.os.IRecoverySystem; import android.os.IRecoverySystemProgressListener; +import android.os.PowerManager; import android.os.RecoverySystem; import android.os.RemoteException; import android.os.SystemProperties; @@ -50,8 +51,15 @@ public final class RecoverySystemService extends SystemService { // The socket at /dev/socket/uncrypt to communicate with uncrypt. private static final String UNCRYPT_SOCKET = "uncrypt"; + // The init services that communicate with /system/bin/uncrypt. + private static final String INIT_SERVICE_UNCRYPT = "init.svc.uncrypt"; + private static final String INIT_SERVICE_SETUP_BCB = "init.svc.setup-bcb"; + private static final String INIT_SERVICE_CLEAR_BCB = "init.svc.clear-bcb"; + private static final int SOCKET_CONNECTION_MAX_RETRY = 30; + private static final Object sRequestLock = new Object(); + private Context mContext; public RecoverySystemService(Context context) { @@ -69,95 +77,155 @@ public final class RecoverySystemService extends SystemService { public boolean uncrypt(String filename, IRecoverySystemProgressListener listener) { if (DEBUG) Slog.d(TAG, "uncrypt: " + filename); - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null); + synchronized (sRequestLock) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null); - // Write the filename into UNCRYPT_PACKAGE_FILE to be read by - // uncrypt. - RecoverySystem.UNCRYPT_PACKAGE_FILE.delete(); + final boolean available = checkAndWaitForUncryptService(); + if (!available) { + Slog.e(TAG, "uncrypt service is unavailable."); + return false; + } - try (FileWriter uncryptFile = new FileWriter(RecoverySystem.UNCRYPT_PACKAGE_FILE)) { - uncryptFile.write(filename + "\n"); - } catch (IOException e) { - Slog.e(TAG, "IOException when writing \"" + RecoverySystem.UNCRYPT_PACKAGE_FILE + - "\": ", e); - return false; - } + // Write the filename into UNCRYPT_PACKAGE_FILE to be read by + // uncrypt. + RecoverySystem.UNCRYPT_PACKAGE_FILE.delete(); - // Trigger uncrypt via init. - SystemProperties.set("ctl.start", "uncrypt"); + try (FileWriter uncryptFile = new FileWriter(RecoverySystem.UNCRYPT_PACKAGE_FILE)) { + uncryptFile.write(filename + "\n"); + } catch (IOException e) { + Slog.e(TAG, "IOException when writing \"" + + RecoverySystem.UNCRYPT_PACKAGE_FILE + "\":", e); + return false; + } - // Connect to the uncrypt service socket. - LocalSocket socket = connectService(); - if (socket == null) { - Slog.e(TAG, "Failed to connect to uncrypt socket"); - return false; - } + // Trigger uncrypt via init. + SystemProperties.set("ctl.start", "uncrypt"); - // Read the status from the socket. - DataInputStream dis = null; - DataOutputStream dos = null; - try { - dis = new DataInputStream(socket.getInputStream()); - dos = new DataOutputStream(socket.getOutputStream()); - int lastStatus = Integer.MIN_VALUE; - while (true) { - int status = dis.readInt(); - // Avoid flooding the log with the same message. - if (status == lastStatus && lastStatus != Integer.MIN_VALUE) { - continue; - } - lastStatus = status; - - if (status >= 0 && status <= 100) { - // Update status - Slog.i(TAG, "uncrypt read status: " + status); - if (listener != null) { - try { - listener.onProgress(status); - } catch (RemoteException ignored) { - Slog.w(TAG, "RemoteException when posting progress"); - } + // Connect to the uncrypt service socket. + LocalSocket socket = connectService(); + if (socket == null) { + Slog.e(TAG, "Failed to connect to uncrypt socket"); + return false; + } + + // Read the status from the socket. + DataInputStream dis = null; + DataOutputStream dos = null; + try { + dis = new DataInputStream(socket.getInputStream()); + dos = new DataOutputStream(socket.getOutputStream()); + int lastStatus = Integer.MIN_VALUE; + while (true) { + int status = dis.readInt(); + // Avoid flooding the log with the same message. + if (status == lastStatus && lastStatus != Integer.MIN_VALUE) { + continue; } - if (status == 100) { - Slog.i(TAG, "uncrypt successfully finished."); - // Ack receipt of the final status code. uncrypt - // waits for the ack so the socket won't be - // destroyed before we receive the code. + lastStatus = status; + + if (status >= 0 && status <= 100) { + // Update status + Slog.i(TAG, "uncrypt read status: " + status); + if (listener != null) { + try { + listener.onProgress(status); + } catch (RemoteException ignored) { + Slog.w(TAG, "RemoteException when posting progress"); + } + } + if (status == 100) { + Slog.i(TAG, "uncrypt successfully finished."); + // Ack receipt of the final status code. uncrypt + // waits for the ack so the socket won't be + // destroyed before we receive the code. + dos.writeInt(0); + break; + } + } else { + // Error in /system/bin/uncrypt. + Slog.e(TAG, "uncrypt failed with status: " + status); + // Ack receipt of the final status code. uncrypt waits + // for the ack so the socket won't be destroyed before + // we receive the code. dos.writeInt(0); - break; + return false; } - } else { - // Error in /system/bin/uncrypt. - Slog.e(TAG, "uncrypt failed with status: " + status); - // Ack receipt of the final status code. uncrypt waits - // for the ack so the socket won't be destroyed before - // we receive the code. - dos.writeInt(0); - return false; } + } catch (IOException e) { + Slog.e(TAG, "IOException when reading status: ", e); + return false; + } finally { + IoUtils.closeQuietly(dis); + IoUtils.closeQuietly(dos); + IoUtils.closeQuietly(socket); } - } catch (IOException e) { - Slog.e(TAG, "IOException when reading status: ", e); - return false; - } finally { - IoUtils.closeQuietly(dis); - IoUtils.closeQuietly(dos); - IoUtils.closeQuietly(socket); - } - return true; + return true; + } } @Override // Binder call public boolean clearBcb() { if (DEBUG) Slog.d(TAG, "clearBcb"); - return setupOrClearBcb(false, null); + synchronized (sRequestLock) { + return setupOrClearBcb(false, null); + } } @Override // Binder call public boolean setupBcb(String command) { if (DEBUG) Slog.d(TAG, "setupBcb: [" + command + "]"); - return setupOrClearBcb(true, command); + synchronized (sRequestLock) { + return setupOrClearBcb(true, command); + } + } + + @Override // Binder call + public void rebootRecoveryWithCommand(String command) { + if (DEBUG) Slog.d(TAG, "rebootRecoveryWithCommand: [" + command + "]"); + synchronized (sRequestLock) { + if (!setupOrClearBcb(true, command)) { + return; + } + + // Having set up the BCB, go ahead and reboot. + PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + pm.reboot(PowerManager.REBOOT_RECOVERY); + } + } + + /** + * Check if any of the init services is still running. If so, we cannot + * start a new uncrypt/setup-bcb/clear-bcb service right away; otherwise + * it may break the socket communication since init creates / deletes + * the socket (/dev/socket/uncrypt) on service start / exit. + */ + private boolean checkAndWaitForUncryptService() { + for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) { + final String uncryptService = SystemProperties.get(INIT_SERVICE_UNCRYPT); + final String setupBcbService = SystemProperties.get(INIT_SERVICE_SETUP_BCB); + final String clearBcbService = SystemProperties.get(INIT_SERVICE_CLEAR_BCB); + final boolean busy = "running".equals(uncryptService) || + "running".equals(setupBcbService) || "running".equals(clearBcbService); + if (DEBUG) { + Slog.i(TAG, "retry: " + retry + " busy: " + busy + + " uncrypt: [" + uncryptService + "]" + + " setupBcb: [" + setupBcbService + "]" + + " clearBcb: [" + clearBcbService + "]"); + } + + if (!busy) { + return true; + } + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Slog.w(TAG, "Interrupted:", e); + } + } + + return false; } private LocalSocket connectService() { @@ -176,7 +244,7 @@ public final class RecoverySystemService extends SystemService { try { Thread.sleep(1000); } catch (InterruptedException e) { - Slog.w(TAG, "Interrupted: ", e); + Slog.w(TAG, "Interrupted:", e); } } } @@ -190,6 +258,12 @@ public final class RecoverySystemService extends SystemService { private boolean setupOrClearBcb(boolean isSetup, String command) { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null); + final boolean available = checkAndWaitForUncryptService(); + if (!available) { + Slog.e(TAG, "uncrypt service is unavailable."); + return false; + } + if (isSetup) { SystemProperties.set("ctl.start", "setup-bcb"); } else { @@ -232,7 +306,7 @@ public final class RecoverySystemService extends SystemService { return false; } } catch (IOException e) { - Slog.e(TAG, "IOException when communicating with uncrypt: ", e); + Slog.e(TAG, "IOException when communicating with uncrypt:", e); return false; } finally { IoUtils.closeQuietly(dis); diff --git a/services/core/java/com/android/server/accounts/AccountManagerBackupHelper.java b/services/core/java/com/android/server/accounts/AccountManagerBackupHelper.java index 63afcccc5961..c3b7e15e43b6 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerBackupHelper.java +++ b/services/core/java/com/android/server/accounts/AccountManagerBackupHelper.java @@ -142,9 +142,8 @@ public final class AccountManagerBackupHelper { final AccountManagerService.UserAccounts accounts = mAccountManagerService .getUserAccounts(userId); synchronized (accounts.cacheLock) { - SQLiteDatabase db = accounts.openHelper.getReadableDatabase(); - List<Pair<String, Integer>> allAccountGrants = DeDatabaseHelper.findAllAccountGrants( - db); + List<Pair<String, Integer>> allAccountGrants = accounts.accountsDb + .findAllAccountGrants(); if (allAccountGrants.isEmpty()) { return null; } diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index f960d8abc7d4..78025764b07a 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -77,7 +77,6 @@ import android.os.Parcel; import android.os.Process; import android.os.RemoteCallback; import android.os.RemoteException; -import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; @@ -99,9 +98,6 @@ import com.android.internal.util.Preconditions; import com.android.server.LocalServices; import com.android.server.ServiceThread; import com.android.server.SystemService; -import com.android.server.accounts.AccountsDb.CeDatabaseHelper; -import com.android.server.accounts.AccountsDb.DeDatabaseHelper; -import com.android.server.accounts.AccountsDb.DebugDbHelper; import com.google.android.collect.Lists; import com.google.android.collect.Sets; @@ -152,7 +148,7 @@ public class AccountManagerService @Override public void onStart() { - mService = new AccountManagerService(getContext()); + mService = new AccountManagerService(new Injector(getContext())); publishBinderService(Context.ACCOUNT_SERVICE, mService); } @@ -162,13 +158,12 @@ public class AccountManagerService } } - private static final int MAX_DEBUG_DB_SIZE = 64; - final Context mContext; private final PackageManager mPackageManager; private final AppOpsManager mAppOpsManager; private UserManager mUserManager; + private final Injector mInjector; final MessageHandler mHandler; @@ -193,7 +188,7 @@ public class AccountManagerService static class UserAccounts { private final int userId; - final DeDatabaseHelper openHelper; + final AccountsDb accountsDb; private final HashMap<Pair<Pair<Account, String>, Integer>, Integer> credentialsPermissionNotificationIds = new HashMap<Pair<Pair<Account, String>, Integer>, Integer>(); @@ -242,12 +237,12 @@ public class AccountManagerService new HashMap<Account, AtomicReference<String>>(); private int debugDbInsertionPoint = -1; - private SQLiteStatement statementForLogging; + private SQLiteStatement statementForLogging; // TODO Move to AccountsDb UserAccounts(Context context, int userId, File preNDbFile, File deDbFile) { this.userId = userId; synchronized (cacheLock) { - openHelper = DeDatabaseHelper.create(context, userId, preNDbFile, deDbFile); + accountsDb = AccountsDb.create(context, userId, preNDbFile, deDbFile); } } } @@ -272,22 +267,13 @@ public class AccountManagerService return sThis.get(); } - public AccountManagerService(Context context) { - this(context, context.getPackageManager(), new AccountAuthenticatorCache(context)); - } - - public AccountManagerService(Context context, PackageManager packageManager, - IAccountAuthenticatorCache authenticatorCache) { - mContext = context; - mPackageManager = packageManager; + public AccountManagerService(Injector injector) { + mInjector = injector; + mContext = injector.getContext(); + mPackageManager = mContext.getPackageManager(); mAppOpsManager = mContext.getSystemService(AppOpsManager.class); - - ServiceThread serviceThread = new ServiceThread(TAG, - android.os.Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */); - serviceThread.start(); - mHandler = new MessageHandler(serviceThread.getLooper()); - - mAuthenticatorCache = authenticatorCache; + mHandler = new MessageHandler(injector.getMessageHandlerLooper()); + mAuthenticatorCache = mInjector.getAccountAuthenticatorCache(); mAuthenticatorCache.setListener(this, null /* Handler */); sThis.set(this); @@ -373,7 +359,7 @@ public class AccountManagerService } }, UserHandle.ALL, userFilter, null, null); - LocalServices.addService(AccountManagerInternal.class, new AccountManagerInternalImpl()); + injector.addLocalService(new AccountManagerInternalImpl()); // Need to cancel account request notifications if the update/install can access the account new PackageMonitor() { @@ -424,7 +410,7 @@ public class AccountManagerService final long identity = Binder.clearCallingIdentity(); try { for (String packageName : packageNames) { - if (mContext.getPackageManager().checkPermission( + if (mPackageManager.checkPermission( Manifest.permission.GET_ACCOUNTS, packageName) != PackageManager.PERMISSION_GRANTED) { continue; @@ -710,8 +696,7 @@ public class AccountManagerService * installed on the device. */ private void addRequestsForPreInstalledApplications() { - List<PackageInfo> allInstalledPackages = mContext.getPackageManager(). - getInstalledPackages(0); + List<PackageInfo> allInstalledPackages = mPackageManager.getInstalledPackages(0); for(PackageInfo pi : allInstalledPackages) { int currentUid = pi.applicationInfo.uid; if(currentUid != -1) { @@ -1003,7 +988,7 @@ public class AccountManagerService UserAccounts accounts, boolean invalidateAuthenticatorCache) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "validateAccountsInternal " + accounts.userId - + " isCeDatabaseAttached=" + accounts.openHelper.isCeDatabaseAttached() + + " isCeDatabaseAttached=" + accounts.accountsDb.isCeDatabaseAttached() + " userLocked=" + mLocalUnlockedUsers.get(accounts.userId)); } @@ -1016,11 +1001,11 @@ public class AccountManagerService boolean userUnlocked = isLocalUnlockedUser(accounts.userId); synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); boolean accountDeleted = false; // Get a map of stored authenticator types to UID - Map<String, Integer> metaAuthUid = DeDatabaseHelper.findMetaAuthUid(db); + final AccountsDb accountsDb = accounts.accountsDb; + Map<String, Integer> metaAuthUid = accountsDb.findMetaAuthUid(); // Create a list of authenticator type whose previous uid no longer exists HashSet<String> obsoleteAuthType = Sets.newHashSet(); SparseBooleanArray knownUids = null; @@ -1057,7 +1042,7 @@ public class AccountManagerService // So purge its data from the account databases. obsoleteAuthType.add(type); // And delete it from the TABLE_META - DeDatabaseHelper.deleteMetaByAuthTypeAndUid(db, type, uid); + accountsDb.deleteMetaByAuthTypeAndUid(type, uid); } } } @@ -1066,11 +1051,10 @@ public class AccountManagerService // been re-enabled (after being updated for example), then we just overwrite the old // values. for (Entry<String, Integer> entry : knownAuth.entrySet()) { - DeDatabaseHelper.insertOrReplaceMetaAuthTypeAndUid(db, entry.getKey(), - entry.getValue()); + accountsDb.insertOrReplaceMetaAuthTypeAndUid(entry.getKey(), entry.getValue()); } - final Map<Long, Account> accountsMap = DeDatabaseHelper.findAllAccounts(db); + final Map<Long, Account> accountsMap = accountsDb.findAllDeAccounts(); try { accounts.accountCache.clear(); final HashMap<String, ArrayList<String>> accountNamesByType = new LinkedHashMap<>(); @@ -1080,17 +1064,17 @@ public class AccountManagerService if (obsoleteAuthType.contains(account.type)) { Slog.w(TAG, "deleting account " + account.name + " because type " + account.type + "'s registered authenticator no longer exist."); - db.beginTransaction(); + accountsDb.beginTransaction(); try { - DeDatabaseHelper.deleteAccount(db, accountId); + accountsDb.deleteDeAccount(accountId); // Also delete from CE table if user is unlocked; if user is currently // locked the account will be removed later by syncDeCeAccountsLocked if (userUnlocked) { - AccountsDb.deleteCeAccount(db, accountId); + accountsDb.deleteCeAccount(accountId); } - db.setTransactionSuccessful(); + accountsDb.setTransactionSuccessful(); } finally { - db.endTransaction(); + accountsDb.endTransaction(); } accountDeleted = true; @@ -1170,23 +1154,20 @@ public class AccountManagerService UserAccounts accounts = mUsers.get(userId); boolean validateAccounts = false; if (accounts == null) { - File preNDbFile = new File(getPreNDatabaseName(userId)); - File deDbFile = new File(getDeDatabaseName(userId)); + File preNDbFile = new File(mInjector.getPreNDatabaseName(userId)); + File deDbFile = new File(mInjector.getDeDatabaseName(userId)); accounts = new UserAccounts(mContext, userId, preNDbFile, deDbFile); - initializeDebugDbSizeAndCompileSqlStatementForLogging( - accounts.openHelper.getWritableDatabase(), accounts); + initializeDebugDbSizeAndCompileSqlStatementForLogging(accounts); mUsers.append(userId, accounts); purgeOldGrants(accounts); validateAccounts = true; } // open CE database if necessary - if (!accounts.openHelper.isCeDatabaseAttached() && mLocalUnlockedUsers.get(userId)) { + if (!accounts.accountsDb.isCeDatabaseAttached() && mLocalUnlockedUsers.get(userId)) { Log.i(TAG, "User " + userId + " is unlocked - opening CE database"); synchronized (accounts.cacheLock) { - File preNDatabaseFile = new File(getPreNDatabaseName(userId)); - File ceDatabaseFile = new File(getCeDatabaseName(userId)); - CeDatabaseHelper.create(mContext, userId, preNDatabaseFile, ceDatabaseFile); - accounts.openHelper.attachCeDatabase(ceDatabaseFile); + File ceDatabaseFile = new File(mInjector.getCeDatabaseName(userId)); + accounts.accountsDb.attachCeDatabase(ceDatabaseFile); } syncDeCeAccountsLocked(accounts); } @@ -1199,8 +1180,7 @@ public class AccountManagerService private void syncDeCeAccountsLocked(UserAccounts accounts) { Preconditions.checkState(Thread.holdsLock(mUsers), "mUsers lock must be held"); - final SQLiteDatabase db = accounts.openHelper.getReadableDatabaseUserIsUnlocked(); - List<Account> accountsToRemove = AccountsDb.findCeAccountsNotInDe(db); + List<Account> accountsToRemove = accounts.accountsDb.findCeAccountsNotInDe(); if (!accountsToRemove.isEmpty()) { Slog.i(TAG, "Accounts " + accountsToRemove + " were previously deleted while user " + accounts.userId + " was locked. Removing accounts from CE tables"); @@ -1223,8 +1203,7 @@ public class AccountManagerService private void purgeOldGrants(UserAccounts accounts) { synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - List<Integer> uids = DeDatabaseHelper.findAllUidGrants(db); + List<Integer> uids = accounts.accountsDb.findAllUidGrants(); for (int uid : uids) { final boolean packageExists = mPackageManager.getPackagesForUid(uid) != null; if (packageExists) { @@ -1232,7 +1211,7 @@ public class AccountManagerService } Log.d(TAG, "deleting grants for UID " + uid + " because its package is no longer installed"); - DeDatabaseHelper.deleteGrantsByUid(db, uid); + accounts.accountsDb.deleteGrantsByUid(uid); } } } @@ -1251,17 +1230,17 @@ public class AccountManagerService } if (accounts != null) { synchronized (accounts.cacheLock) { - accounts.openHelper.close(); + accounts.accountsDb.close(); } } Log.i(TAG, "Removing database files for user " + userId); - File dbFile = new File(getDeDatabaseName(userId)); + File dbFile = new File(mInjector.getDeDatabaseName(userId)); AccountsDb.deleteDbFileWarnIfFailed(dbFile); // Remove CE file if user is unlocked, or FBE is not enabled boolean fbeEnabled = StorageManager.isFileEncryptedNativeOrEmulated(); if (!fbeEnabled || userUnlocked) { - File ceDb = new File(getCeDatabaseName(userId)); + File ceDb = new File(mInjector.getCeDatabaseName(userId)); if (ceDb.exists()) { AccountsDb.deleteDbFileWarnIfFailed(ceDb); } @@ -1344,9 +1323,7 @@ public class AccountManagerService } synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getReadableDatabaseUserIsUnlocked(); - return CeDatabaseHelper.findAccountPasswordByNameAndType(db, account.name, - account.type); + return accounts.accountsDb.findAccountPasswordByNameAndType(account.name, account.type); } } @@ -1375,8 +1352,7 @@ public class AccountManagerService synchronized (accounts.cacheLock) { AtomicReference<String> previousNameRef = accounts.previousNameCache.get(account); if (previousNameRef == null) { - final SQLiteDatabase db = accounts.openHelper.getReadableDatabase(); - String previousName = DeDatabaseHelper.findAccountPreviousName(db, account); + String previousName = accounts.accountsDb.findDeAccountPreviousName(account); previousNameRef = new AtomicReference<>(previousName); accounts.previousNameCache.put(account, previousNameRef); return previousName; @@ -1612,8 +1588,7 @@ public class AccountManagerService private boolean updateLastAuthenticatedTime(Account account) { final UserAccounts accounts = getUserAccountsForCaller(); synchronized (accounts.cacheLock) { - return DeDatabaseHelper.updateAccountLastAuthenticatedTime( - accounts.openHelper.getWritableDatabase(), account); + return accounts.accountsDb.updateAccountLastAuthenticatedTime(account); } } @@ -1682,22 +1657,21 @@ public class AccountManagerService return false; } synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabaseUserIsUnlocked(); - db.beginTransaction(); + accounts.accountsDb.beginTransaction(); try { - if (CeDatabaseHelper.findAccountId(db, account) >= 0) { + if (accounts.accountsDb.findCeAccountId(account) >= 0) { Log.w(TAG, "insertAccountIntoDatabase: " + account + ", skipping since the account already exists"); return false; } - long accountId = CeDatabaseHelper.insertAccount(db, account, password); + long accountId = accounts.accountsDb.insertCeAccount(account, password); if (accountId < 0) { Log.w(TAG, "insertAccountIntoDatabase: " + account + ", skipping the DB insert failed"); return false; } // Insert into DE table - if (DeDatabaseHelper.insertAccount(db, account, accountId) < 0) { + if (accounts.accountsDb.insertDeAccount(account, accountId) < 0) { Log.w(TAG, "insertAccountIntoDatabase: " + account + ", skipping the DB insert failed"); return false; @@ -1705,21 +1679,21 @@ public class AccountManagerService if (extras != null) { for (String key : extras.keySet()) { final String value = extras.getString(key); - if (CeDatabaseHelper.insertExtra(db, accountId, key, value) < 0) { + if (accounts.accountsDb.insertExtra(accountId, key, value) < 0) { Log.w(TAG, "insertAccountIntoDatabase: " + account + ", skipping since insertExtra failed for key " + key); return false; } } } - db.setTransactionSuccessful(); + accounts.accountsDb.setTransactionSuccessful(); logRecord(AccountsDb.DEBUG_ACTION_ACCOUNT_ADD, AccountsDb.TABLE_ACCOUNTS, accountId, accounts, callingUid); insertAccountIntoCacheLocked(accounts, account); } finally { - db.endTransaction(); + accounts.accountsDb.endTransaction(); } } if (getUserManager().getUserInfo(accounts.userId).canHaveProfile()) { @@ -1902,17 +1876,16 @@ public class AccountManagerService } } synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabaseUserIsUnlocked(); - db.beginTransaction(); + accounts.accountsDb.beginTransaction(); Account renamedAccount = new Account(newName, accountToRename.type); try { - final long accountId = DeDatabaseHelper.findAccountId(db, accountToRename); + final long accountId = accounts.accountsDb.findDeAccountId(accountToRename); if (accountId >= 0) { - CeDatabaseHelper.renameAccount(db, accountId, newName); - DeDatabaseHelper.renameAccount(db, accountId, newName, accountToRename.name); + accounts.accountsDb.renameCeAccount(accountId, newName); + accounts.accountsDb.renameDeAccount(accountId, newName, accountToRename.name); } } finally { - db.endTransaction(); + accounts.accountsDb.endTransaction(); } /* * Database transaction was successful. Clean up cached @@ -2035,8 +2008,7 @@ public class AccountManagerService } } } - SQLiteDatabase db = accounts.openHelper.getReadableDatabase(); - final long accountId = DeDatabaseHelper.findAccountId(db, account); + final long accountId = accounts.accountsDb.findDeAccountId(account); logRecord( AccountsDb.DEBUG_ACTION_CALLED_ACCOUNT_REMOVE, AccountsDb.TABLE_ACCOUNTS, @@ -2075,8 +2047,7 @@ public class AccountManagerService } removeVisibleListFunctionality(account, getUserAccounts(UserHandle.getUserId(callingUid))); UserAccounts accounts = getUserAccountsForCaller(); - SQLiteDatabase db = accounts.openHelper.getReadableDatabase(); - final long accountId = DeDatabaseHelper.findAccountId(db, account); + final long accountId = accounts.accountsDb.findDeAccountId(account); logRecord( AccountsDb.DEBUG_ACTION_CALLED_ACCOUNT_REMOVE, AccountsDb.TABLE_ACCOUNTS, @@ -2153,26 +2124,23 @@ public class AccountManagerService + " is still locked. CE data will be removed later"); } synchronized (accounts.cacheLock) { - final SQLiteDatabase db = userUnlocked - ? accounts.openHelper.getWritableDatabaseUserIsUnlocked() - : accounts.openHelper.getWritableDatabase(); - db.beginTransaction(); + accounts.accountsDb.beginTransaction(); // Set to a dummy value, this will only be used if the database // transaction succeeds. long accountId = -1; try { - accountId = DeDatabaseHelper.findAccountId(db, account); + accountId = accounts.accountsDb.findDeAccountId(account); if (accountId >= 0) { - DeDatabaseHelper.deleteAccount(db, accountId); + accounts.accountsDb.deleteDeAccount(accountId); if (userUnlocked) { // Delete from CE table - AccountsDb.deleteCeAccount(db, accountId); + accounts.accountsDb.deleteCeAccount(accountId); } - db.setTransactionSuccessful(); + accounts.accountsDb.setTransactionSuccessful(); isChanged = true; } } finally { - db.endTransaction(); + accounts.accountsDb.endTransaction(); } if (isChanged) { removeAccountFromCacheLocked(accounts, account); @@ -2231,14 +2199,13 @@ public class AccountManagerService try { UserAccounts accounts = getUserAccounts(userId); synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabaseUserIsUnlocked(); - db.beginTransaction(); + accounts.accountsDb.beginTransaction(); try { - invalidateAuthTokenLocked(accounts, db, accountType, authToken); + invalidateAuthTokenLocked(accounts, accountType, authToken); invalidateCustomTokenLocked(accounts, accountType, authToken); - db.setTransactionSuccessful(); + accounts.accountsDb.setTransactionSuccessful(); } finally { - db.endTransaction(); + accounts.accountsDb.endTransaction(); } } } finally { @@ -2257,21 +2224,20 @@ public class AccountManagerService accounts.accountTokenCaches.remove(accountType, authToken); } - private void invalidateAuthTokenLocked(UserAccounts accounts, SQLiteDatabase db, - String accountType, String authToken) { + private void invalidateAuthTokenLocked(UserAccounts accounts, String accountType, + String authToken) { if (authToken == null || accountType == null) { return; } - Cursor cursor = CeDatabaseHelper.findAuthtokenForAllAccounts(db, accountType, authToken); + Cursor cursor = accounts.accountsDb.findAuthtokenForAllAccounts(accountType, authToken); try { while (cursor.moveToNext()) { String authTokenId = cursor.getString(0); String accountName = cursor.getString(1); String authTokenType = cursor.getString(2); - CeDatabaseHelper.deleteAuthToken(db, authTokenId); + accounts.accountsDb.deleteAuthToken(authTokenId); writeAuthTokenIntoCacheLocked( accounts, - db, new Account(accountName, accountType), authTokenType, null); @@ -2309,22 +2275,21 @@ public class AccountManagerService cancelNotification(getSigninRequiredNotificationId(accounts, account), UserHandle.of(accounts.userId)); synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabaseUserIsUnlocked(); - db.beginTransaction(); + accounts.accountsDb.beginTransaction(); try { - long accountId = DeDatabaseHelper.findAccountId(db, account); + long accountId = accounts.accountsDb.findDeAccountId(account); if (accountId < 0) { return false; } - CeDatabaseHelper.deleteAuthtokensByAccountIdAndType(db, accountId, type); - if (CeDatabaseHelper.insertAuthToken(db, accountId, type, authToken) >= 0) { - db.setTransactionSuccessful(); - writeAuthTokenIntoCacheLocked(accounts, db, account, type, authToken); + accounts.accountsDb.deleteAuthtokensByAccountIdAndType(accountId, type); + if (accounts.accountsDb.insertAuthToken(accountId, type, authToken) >= 0) { + accounts.accountsDb.setTransactionSuccessful(); + writeAuthTokenIntoCacheLocked(accounts, account, type, authToken); return true; } return false; } finally { - db.endTransaction(); + accounts.accountsDb.endTransaction(); } } } @@ -2423,16 +2388,15 @@ public class AccountManagerService } boolean isChanged = false; synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabaseUserIsUnlocked(); - db.beginTransaction(); + accounts.accountsDb.beginTransaction(); try { - final long accountId = DeDatabaseHelper.findAccountId(db, account); + final long accountId = accounts.accountsDb.findDeAccountId(account); if (accountId >= 0) { - CeDatabaseHelper.updateAccountPassword(db, accountId, password); - CeDatabaseHelper.deleteAuthTokensByAccountId(db, accountId); + accounts.accountsDb.updateCeAccountPassword(accountId, password); + accounts.accountsDb.deleteAuthTokensByAccountId(accountId); accounts.authTokenCache.remove(account); accounts.accountTokenCaches.remove(account); - db.setTransactionSuccessful(); + accounts.accountsDb.setTransactionSuccessful(); // If there is an account whose password will be updated and the database // transactions succeed, then we say that a change has occured. Even if the // new password is the same as the old and there were no authtokens to delete. @@ -2443,7 +2407,7 @@ public class AccountManagerService logRecord(action, AccountsDb.TABLE_ACCOUNTS, accountId, accounts, callingUid); } } finally { - db.endTransaction(); + accounts.accountsDb.endTransaction(); if (isChanged) { // Send LOGIN_ACCOUNTS_CHANGED only if the something changed. sendAccountsChangedBroadcast(accounts.userId); @@ -2533,26 +2497,25 @@ public class AccountManagerService if (account == null || key == null) { return; } - final SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - db.beginTransaction(); + accounts.accountsDb.beginTransaction(); try { - long accountId = DeDatabaseHelper.findAccountId(db, account); + long accountId = accounts.accountsDb.findDeAccountId(account); if (accountId < 0) { return; } - long extrasId = CeDatabaseHelper.findExtrasIdByAccountId(db, accountId, key); + long extrasId = accounts.accountsDb.findExtrasIdByAccountId(accountId, key); if (extrasId < 0) { - extrasId = CeDatabaseHelper.insertExtra(db, accountId, key, value); + extrasId = accounts.accountsDb.insertExtra(accountId, key, value); if (extrasId < 0) { return; } - } else if (!CeDatabaseHelper.updateExtra(db, extrasId, value)) { + } else if (!accounts.accountsDb.updateExtra(extrasId, value)) { return; } - writeUserDataIntoCacheLocked(accounts, db, account, key, value); - db.setTransactionSuccessful(); + writeUserDataIntoCacheLocked(accounts, account, key, value); + accounts.accountsDb.setTransactionSuccessful(); } finally { - db.endTransaction(); + accounts.accountsDb.endTransaction(); } } @@ -4181,9 +4144,8 @@ public class AccountManagerService private boolean addSharedAccountAsUser(Account account, int userId) { userId = handleIncomingUser(userId); UserAccounts accounts = getUserAccounts(userId); - SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - DeDatabaseHelper.deleteSharedAccount(db, account); - long accountId = DeDatabaseHelper.insertSharedAccount(db, account); + accounts.accountsDb.deleteSharedAccount(account); + long accountId = accounts.accountsDb.insertSharedAccount(account); if (accountId < 0) { Log.w(TAG, "insertAccountIntoDatabase: " + account + ", skipping the DB insert failed"); @@ -4198,9 +4160,8 @@ public class AccountManagerService public boolean renameSharedAccountAsUser(Account account, String newName, int userId) { userId = handleIncomingUser(userId); UserAccounts accounts = getUserAccounts(userId); - SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - long sharedTableAccountId = DeDatabaseHelper.findSharedAccountId(db, account); - int r = DeDatabaseHelper.renameSharedAccount(db, account, newName); + long sharedTableAccountId = accounts.accountsDb.findSharedAccountId(account); + int r = accounts.accountsDb.renameSharedAccount(account, newName); if (r > 0) { int callingUid = getCallingUid(); logRecord(AccountsDb.DEBUG_ACTION_ACCOUNT_RENAME, AccountsDb.TABLE_SHARED_ACCOUNTS, @@ -4219,9 +4180,8 @@ public class AccountManagerService private boolean removeSharedAccountAsUser(Account account, int userId, int callingUid) { userId = handleIncomingUser(userId); UserAccounts accounts = getUserAccounts(userId); - SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - long sharedTableAccountId = DeDatabaseHelper.findSharedAccountId(db, account); - boolean deleted = DeDatabaseHelper.deleteSharedAccount(db, account); + long sharedTableAccountId = accounts.accountsDb.findSharedAccountId(account); + boolean deleted = accounts.accountsDb.deleteSharedAccount(account); if (deleted) { logRecord(AccountsDb.DEBUG_ACTION_ACCOUNT_REMOVE, AccountsDb.TABLE_SHARED_ACCOUNTS, sharedTableAccountId, accounts, callingUid); @@ -4233,8 +4193,8 @@ public class AccountManagerService @Override public Account[] getSharedAccountsAsUser(int userId) { userId = handleIncomingUser(userId); - SQLiteDatabase db = getUserAccounts(userId).openHelper.getReadableDatabase(); - List<Account> accountList = DeDatabaseHelper.getSharedAccounts(db); + UserAccounts accounts = getUserAccounts(userId); + List<Account> accountList = accounts.accountsDb.getSharedAccounts(); Account[] accountArray = new Account[accountList.size()]; accountList.toArray(accountArray); return accountArray; @@ -4585,9 +4545,8 @@ public class AccountManagerService if (mAuthDetailsRequired) { long lastAuthenticatedTime = -1; if (accountPresent) { - lastAuthenticatedTime = DeDatabaseHelper + lastAuthenticatedTime = mAccounts.accountsDb .findAccountLastAuthenticatedTime( - mAccounts.openHelper.getReadableDatabase(), new Account(mAccountName, mAccountType)); } result.putLong(AccountManager.KEY_LAST_AUTHENTICATED_TIME, @@ -4745,47 +4704,6 @@ public class AccountManagerService } } - @VisibleForTesting - String getPreNDatabaseName(int userId) { - File systemDir = Environment.getDataSystemDirectory(); - File databaseFile = new File(Environment.getUserSystemDirectory(userId), - PRE_N_DATABASE_NAME); - if (userId == 0) { - // Migrate old file, if it exists, to the new location. - // Make sure the new file doesn't already exist. A dummy file could have been - // accidentally created in the old location, causing the new one to become corrupted - // as well. - File oldFile = new File(systemDir, PRE_N_DATABASE_NAME); - if (oldFile.exists() && !databaseFile.exists()) { - // Check for use directory; create if it doesn't exist, else renameTo will fail - File userDir = Environment.getUserSystemDirectory(userId); - if (!userDir.exists()) { - if (!userDir.mkdirs()) { - throw new IllegalStateException("User dir cannot be created: " + userDir); - } - } - if (!oldFile.renameTo(databaseFile)) { - throw new IllegalStateException("User dir cannot be migrated: " + databaseFile); - } - } - } - return databaseFile.getPath(); - } - - @VisibleForTesting - String getDeDatabaseName(int userId) { - File databaseFile = new File(Environment.getDataSystemDeDirectory(userId), - AccountsDb.DE_DATABASE_NAME); - return databaseFile.getPath(); - } - - @VisibleForTesting - String getCeDatabaseName(int userId) { - File databaseFile = new File(Environment.getDataSystemCeDirectory(userId), - AccountsDb.CE_DATABASE_NAME); - return databaseFile.getPath(); - } - private void logRecord(UserAccounts accounts, String action, String tableName) { logRecord(action, tableName, -1, accounts); } @@ -4846,7 +4764,7 @@ public class AccountManagerService LogRecordTask logTask = new LogRecordTask(action, tableName, accountId, userAccount, callingUid, userAccount.debugDbInsertionPoint); userAccount.debugDbInsertionPoint = (userAccount.debugDbInsertionPoint + 1) - % MAX_DEBUG_DB_SIZE; + % AccountsDb.MAX_DEBUG_DB_SIZE; mHandler.post(logTask); } @@ -4854,17 +4772,10 @@ public class AccountManagerService * This should only be called once to compile the sql statement for logging * and to find the insertion point. */ - private void initializeDebugDbSizeAndCompileSqlStatementForLogging(SQLiteDatabase db, - UserAccounts userAccount) { - // Initialize the count if not done earlier. - int size = DebugDbHelper.getDebugTableRowCount(db); - if (size >= MAX_DEBUG_DB_SIZE) { - // Table is full, and we need to find the point where to insert. - userAccount.debugDbInsertionPoint = DebugDbHelper.getDebugTableInsertionPoint(db); - } else { - userAccount.debugDbInsertionPoint = size; - } - userAccount.statementForLogging = DebugDbHelper.compileSqlStatementForLogging(db); + private void initializeDebugDbSizeAndCompileSqlStatementForLogging(UserAccounts userAccount) { + userAccount.debugDbInsertionPoint = userAccount.accountsDb + .calculateDebugTableInsertionPoint(); + userAccount.statementForLogging = userAccount.accountsDb.compileSqlStatementForLogging(); } public IBinder onBind(@SuppressWarnings("unused") Intent intent) { @@ -4913,11 +4824,9 @@ public class AccountManagerService private void dumpUser(UserAccounts userAccounts, FileDescriptor fd, PrintWriter fout, String[] args, boolean isCheckinRequest) { synchronized (userAccounts.cacheLock) { - final SQLiteDatabase db = userAccounts.openHelper.getReadableDatabase(); - if (isCheckinRequest) { // This is a checkin request. *Only* upload the account types and the count of each. - DeDatabaseHelper.dumpAccountsTable(db, fout); + userAccounts.accountsDb.dumpDeAccountsTable(fout); } else { Account[] accounts = getAccountsFromCacheLocked(userAccounts, null /* type */, Process.myUid(), null); @@ -4928,7 +4837,7 @@ public class AccountManagerService // Add debug information. fout.println(); - DebugDbHelper.dumpDebugTable(db, fout); + userAccounts.accountsDb.dumpDebugTable(fout); fout.println(); synchronized (mSessions) { final long now = SystemClock.elapsedRealtime(); @@ -4981,17 +4890,11 @@ public class AccountManagerService } } - @VisibleForTesting - protected void installNotification(int notificationId, final Notification notification, - UserHandle user) { - installNotification(notificationId, notification, "android", user.getIdentifier()); - } - private void installNotification(int notificationId, final Notification notification, String packageName, int userId) { final long token = clearCallingIdentity(); try { - INotificationManager notificationManager = NotificationManager.getService(); + INotificationManager notificationManager = mInjector.getNotificationManager(); try { notificationManager.enqueueNotificationWithTag(packageName, packageName, null, notificationId, notification, new int[1], userId); @@ -5003,16 +4906,14 @@ public class AccountManagerService } } - @VisibleForTesting - protected void cancelNotification(int id, UserHandle user) { + private void cancelNotification(int id, UserHandle user) { cancelNotification(id, mContext.getPackageName(), user); } - protected void cancelNotification(int id, String packageName, UserHandle user) { + private void cancelNotification(int id, String packageName, UserHandle user) { long identityToken = clearCallingIdentity(); try { - INotificationManager service = INotificationManager.Stub.asInterface( - ServiceManager.getService(Context.NOTIFICATION_SERVICE)); + INotificationManager service = mInjector.getNotificationManager(); service.cancelNotificationWithTag(packageName, null, id, user.getIdentifier()); } catch (RemoteException e) { /* ignore - local call */ @@ -5198,13 +5099,12 @@ public class AccountManagerService } UserAccounts accounts = getUserAccounts(UserHandle.getUserId(callerUid)); synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getReadableDatabase(); long grantsCount; if (authTokenType != null) { - grantsCount = DeDatabaseHelper.findMatchingGrantsCount(db, callerUid, authTokenType, + grantsCount = accounts.accountsDb.findMatchingGrantsCount(callerUid, authTokenType, account); } else { - grantsCount = DeDatabaseHelper.findMatchingGrantsCountAnyToken(db, callerUid, + grantsCount = accounts.accountsDb.findMatchingGrantsCountAnyToken(callerUid, account); } final boolean permissionGranted = grantsCount > 0; @@ -5332,10 +5232,9 @@ public class AccountManagerService } UserAccounts accounts = getUserAccounts(UserHandle.getUserId(uid)); synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - long accountId = DeDatabaseHelper.findAccountId(db, account); + long accountId = accounts.accountsDb.findDeAccountId(account); if (accountId >= 0) { - DeDatabaseHelper.insertGrant(db, accountId, authTokenType, uid); + accounts.accountsDb.insertGrant(accountId, authTokenType, uid); } cancelNotification(getCredentialPermissionNotificationId(account, authTokenType, uid), UserHandle.of(accounts.userId)); @@ -5365,17 +5264,16 @@ public class AccountManagerService } UserAccounts accounts = getUserAccounts(UserHandle.getUserId(uid)); synchronized (accounts.cacheLock) { - final SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); - db.beginTransaction(); + accounts.accountsDb.beginTransaction(); try { - long accountId = DeDatabaseHelper.findAccountId(db, account); + long accountId = accounts.accountsDb.findDeAccountId(account); if (accountId >= 0) { - DeDatabaseHelper.deleteGrantsByAccountIdAuthTokenTypeAndUid( - db, accountId, authTokenType, uid); - db.setTransactionSuccessful(); + accounts.accountsDb.deleteGrantsByAccountIdAuthTokenTypeAndUid( + accountId, authTokenType, uid); + accounts.accountsDb.setTransactionSuccessful(); } } finally { - db.endTransaction(); + accounts.accountsDb.endTransaction(); } cancelNotification(getCredentialPermissionNotificationId(account, authTokenType, uid), @@ -5531,11 +5429,11 @@ public class AccountManagerService } } - protected void writeUserDataIntoCacheLocked(UserAccounts accounts, final SQLiteDatabase db, + protected void writeUserDataIntoCacheLocked(UserAccounts accounts, Account account, String key, String value) { Map<String, String> userDataForAccount = accounts.userDataCache.get(account); if (userDataForAccount == null) { - userDataForAccount = CeDatabaseHelper.findUserExtrasForAccount(db, account); + userDataForAccount = accounts.accountsDb.findUserExtrasForAccount(account); accounts.userDataCache.put(account, userDataForAccount); } if (value == null) { @@ -5557,11 +5455,11 @@ public class AccountManagerService } } - protected void writeAuthTokenIntoCacheLocked(UserAccounts accounts, final SQLiteDatabase db, + protected void writeAuthTokenIntoCacheLocked(UserAccounts accounts, Account account, String key, String value) { Map<String, String> authTokensForAccount = accounts.authTokenCache.get(account); if (authTokensForAccount == null) { - authTokensForAccount = CeDatabaseHelper.findAuthTokensByAccount(db, account); + authTokensForAccount = accounts.accountsDb.findAuthTokensByAccount(account); accounts.authTokenCache.put(account, authTokensForAccount); } if (value == null) { @@ -5577,8 +5475,7 @@ public class AccountManagerService Map<String, String> authTokensForAccount = accounts.authTokenCache.get(account); if (authTokensForAccount == null) { // need to populate the cache for this account - final SQLiteDatabase db = accounts.openHelper.getReadableDatabaseUserIsUnlocked(); - authTokensForAccount = CeDatabaseHelper.findAuthTokensByAccount(db, account); + authTokensForAccount = accounts.accountsDb.findAuthTokensByAccount(account); accounts.authTokenCache.put(account, authTokensForAccount); } return authTokensForAccount.get(authTokenType); @@ -5590,8 +5487,7 @@ public class AccountManagerService Map<String, String> userDataForAccount = accounts.userDataCache.get(account); if (userDataForAccount == null) { // need to populate the cache for this account - final SQLiteDatabase db = accounts.openHelper.getReadableDatabaseUserIsUnlocked(); - userDataForAccount = CeDatabaseHelper.findUserExtrasForAccount(db, account); + userDataForAccount = accounts.accountsDb.findUserExtrasForAccount(account); accounts.userDataCache.put(account, userDataForAccount); } return userDataForAccount.get(key); @@ -5714,4 +5610,74 @@ public class AccountManagerService } } } + + @VisibleForTesting + static class Injector { + private final Context mContext; + + public Injector(Context context) { + mContext = context; + } + + Looper getMessageHandlerLooper() { + ServiceThread serviceThread = new ServiceThread(TAG, + android.os.Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */); + serviceThread.start(); + return serviceThread.getLooper(); + } + + Context getContext() { + return mContext; + } + + void addLocalService(AccountManagerInternal service) { + LocalServices.addService(AccountManagerInternal.class, service); + } + + String getDeDatabaseName(int userId) { + File databaseFile = new File(Environment.getDataSystemDeDirectory(userId), + AccountsDb.DE_DATABASE_NAME); + return databaseFile.getPath(); + } + + String getCeDatabaseName(int userId) { + File databaseFile = new File(Environment.getDataSystemCeDirectory(userId), + AccountsDb.CE_DATABASE_NAME); + return databaseFile.getPath(); + } + + String getPreNDatabaseName(int userId) { + File systemDir = Environment.getDataSystemDirectory(); + File databaseFile = new File(Environment.getUserSystemDirectory(userId), + PRE_N_DATABASE_NAME); + if (userId == 0) { + // Migrate old file, if it exists, to the new location. + // Make sure the new file doesn't already exist. A dummy file could have been + // accidentally created in the old location, causing the new one to become corrupted + // as well. + File oldFile = new File(systemDir, PRE_N_DATABASE_NAME); + if (oldFile.exists() && !databaseFile.exists()) { + // Check for use directory; create if it doesn't exist, else renameTo will fail + File userDir = Environment.getUserSystemDirectory(userId); + if (!userDir.exists()) { + if (!userDir.mkdirs()) { + throw new IllegalStateException("User dir cannot be created: " + userDir); + } + } + if (!oldFile.renameTo(databaseFile)) { + throw new IllegalStateException("User dir cannot be migrated: " + databaseFile); + } + } + } + return databaseFile.getPath(); + } + + IAccountAuthenticatorCache getAccountAuthenticatorCache() { + return new AccountAuthenticatorCache(mContext); + } + + INotificationManager getNotificationManager() { + return NotificationManager.getService(); + } + } } diff --git a/services/core/java/com/android/server/accounts/AccountsDb.java b/services/core/java/com/android/server/accounts/AccountsDb.java index 6ef521ede4c1..1adcf34fc326 100644 --- a/services/core/java/com/android/server/accounts/AccountsDb.java +++ b/services/core/java/com/android/server/accounts/AccountsDb.java @@ -42,8 +42,13 @@ import java.util.Map; /** * Persistence layer abstraction for accessing accounts_ce/accounts_de databases. + * + * <p>At first, CE database needs to be {@link #attachCeDatabase(File) attached to DE}, + * in order for the tables to be available. All operations with CE database are done through the + * connection to the DE database, to which it is attached. This approach allows atomic + * transactions across two databases</p> */ -class AccountsDb { +class AccountsDb implements AutoCloseable { private static final String TAG = "AccountsDb"; private static final String DATABASE_NAME = "accounts.db"; @@ -128,6 +133,8 @@ class AccountsDb { private static final String CE_TABLE_AUTHTOKENS = CE_DB_PREFIX + TABLE_AUTHTOKENS; private static final String CE_TABLE_EXTRAS = CE_DB_PREFIX + TABLE_EXTRAS; + static final int MAX_DEBUG_DB_SIZE = 64; + private static final String[] ACCOUNT_TYPE_COUNT_PROJECTION = new String[] { ACCOUNTS_TYPE, ACCOUNTS_TYPE_COUNT}; @@ -169,7 +176,17 @@ class AccountsDb { private static final String META_KEY_DELIMITER = ":"; private static final String SELECTION_META_BY_AUTHENTICATOR_TYPE = META_KEY + " LIKE ?"; - static class CeDatabaseHelper extends SQLiteOpenHelper { + private final DeDatabaseHelper mDeDatabase; + private final Context mContext; + private final File mPreNDatabaseFile; + + AccountsDb(DeDatabaseHelper deDatabase, Context context, File preNDatabaseFile) { + mDeDatabase = deDatabase; + mContext = context; + mPreNDatabaseFile = preNDatabaseFile; + } + + private static class CeDatabaseHelper extends SQLiteOpenHelper { CeDatabaseHelper(Context context, String ceDatabaseName) { super(context, ceDatabaseName, null, CE_DATABASE_VERSION); @@ -249,17 +266,16 @@ class AccountsDb { /** * Creates a new {@code CeDatabaseHelper}. If pre-N db file is present at the old location, * it also performs migration to the new CE database. - * @param userId id of the user where the database is located */ static CeDatabaseHelper create( Context context, - int userId, File preNDatabaseFile, File ceDatabaseFile) { boolean newDbExists = ceDatabaseFile.exists(); if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "CeDatabaseHelper.create userId=" + userId + " oldDbExists=" - + preNDatabaseFile.exists() + " newDbExists=" + newDbExists); + Log.v(TAG, "CeDatabaseHelper.create ceDatabaseFile=" + ceDatabaseFile + + " oldDbExists=" + preNDatabaseFile.exists() + + " newDbExists=" + newDbExists); } boolean removeOldDb = false; if (!newDbExists && preNDatabaseFile.exists()) { @@ -290,187 +306,189 @@ class AccountsDb { } return true; } + } - /** - * Returns information about auth tokens and their account for the specified query - * parameters. - * Output is in the format: - * <pre><code> | AUTHTOKEN_ID | ACCOUNT_NAME | AUTH_TOKEN_TYPE |</code></pre> - */ - static Cursor findAuthtokenForAllAccounts(SQLiteDatabase db, String accountType, - String authToken) { - return db.rawQuery( - "SELECT " + CE_TABLE_AUTHTOKENS + "." + AUTHTOKENS_ID - + ", " + CE_TABLE_ACCOUNTS + "." + ACCOUNTS_NAME - + ", " + CE_TABLE_AUTHTOKENS + "." + AUTHTOKENS_TYPE - + " FROM " + CE_TABLE_ACCOUNTS - + " JOIN " + CE_TABLE_AUTHTOKENS - + " ON " + CE_TABLE_ACCOUNTS + "." + ACCOUNTS_ID - + " = " + CE_TABLE_AUTHTOKENS + "." + AUTHTOKENS_ACCOUNTS_ID - + " WHERE " + CE_TABLE_AUTHTOKENS + "." + AUTHTOKENS_AUTHTOKEN - + " = ? AND " + CE_TABLE_ACCOUNTS + "." + ACCOUNTS_TYPE + " = ?", - new String[]{authToken, accountType}); - } + /** + * Returns information about auth tokens and their account for the specified query + * parameters. + * Output is in the format: + * <pre><code> | AUTHTOKEN_ID | ACCOUNT_NAME | AUTH_TOKEN_TYPE |</code></pre> + */ + Cursor findAuthtokenForAllAccounts(String accountType, String authToken) { + SQLiteDatabase db = mDeDatabase.getReadableDatabaseUserIsUnlocked(); + return db.rawQuery( + "SELECT " + CE_TABLE_AUTHTOKENS + "." + AUTHTOKENS_ID + + ", " + CE_TABLE_ACCOUNTS + "." + ACCOUNTS_NAME + + ", " + CE_TABLE_AUTHTOKENS + "." + AUTHTOKENS_TYPE + + " FROM " + CE_TABLE_ACCOUNTS + + " JOIN " + CE_TABLE_AUTHTOKENS + + " ON " + CE_TABLE_ACCOUNTS + "." + ACCOUNTS_ID + + " = " + CE_TABLE_AUTHTOKENS + "." + AUTHTOKENS_ACCOUNTS_ID + + " WHERE " + CE_TABLE_AUTHTOKENS + "." + AUTHTOKENS_AUTHTOKEN + + " = ? AND " + CE_TABLE_ACCOUNTS + "." + ACCOUNTS_TYPE + " = ?", + new String[]{authToken, accountType}); + } - static boolean deleteAuthtokensByAccountIdAndType(SQLiteDatabase db, long accountId, - String authtokenType) { - return db.delete(CE_TABLE_AUTHTOKENS, - AUTHTOKENS_ACCOUNTS_ID + "=?" + accountId + " AND " + AUTHTOKENS_TYPE + "=?", - new String[]{String.valueOf(accountId), authtokenType}) > 0; + Map<String, String> findAuthTokensByAccount(Account account) { + SQLiteDatabase db = mDeDatabase.getReadableDatabaseUserIsUnlocked(); + HashMap<String, String> authTokensForAccount = new HashMap<>(); + Cursor cursor = db.query(CE_TABLE_AUTHTOKENS, + COLUMNS_AUTHTOKENS_TYPE_AND_AUTHTOKEN, + SELECTION_AUTHTOKENS_BY_ACCOUNT, + new String[] {account.name, account.type}, + null, null, null); + try { + while (cursor.moveToNext()) { + final String type = cursor.getString(0); + final String authToken = cursor.getString(1); + authTokensForAccount.put(type, authToken); + } + } finally { + cursor.close(); } + return authTokensForAccount; + } - static boolean deleteAuthToken(SQLiteDatabase db, String authTokenId) { - return db.delete( - CE_TABLE_AUTHTOKENS, AUTHTOKENS_ID + "= ?", - new String[]{authTokenId}) > 0; - } + boolean deleteAuthtokensByAccountIdAndType(long accountId, String authtokenType) { + SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked(); + return db.delete(CE_TABLE_AUTHTOKENS, + AUTHTOKENS_ACCOUNTS_ID + "=?" + accountId + " AND " + AUTHTOKENS_TYPE + "=?", + new String[]{String.valueOf(accountId), authtokenType}) > 0; + } - static long insertAuthToken(SQLiteDatabase db, long accountId, String authTokenType, - String authToken) { - ContentValues values = new ContentValues(); - values.put(AUTHTOKENS_ACCOUNTS_ID, accountId); - values.put(AUTHTOKENS_TYPE, authTokenType); - values.put(AUTHTOKENS_AUTHTOKEN, authToken); - return db.insert( - CE_TABLE_AUTHTOKENS, AUTHTOKENS_AUTHTOKEN, values); - } + boolean deleteAuthToken(String authTokenId) { + SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked(); + return db.delete( + CE_TABLE_AUTHTOKENS, AUTHTOKENS_ID + "= ?", + new String[]{authTokenId}) > 0; + } - static Map<String, String> findAuthTokensByAccount(final SQLiteDatabase db, - Account account) { - HashMap<String, String> authTokensForAccount = new HashMap<>(); - Cursor cursor = db.query(CE_TABLE_AUTHTOKENS, - COLUMNS_AUTHTOKENS_TYPE_AND_AUTHTOKEN, - SELECTION_AUTHTOKENS_BY_ACCOUNT, - new String[]{account.name, account.type}, - null, null, null); - try { - while (cursor.moveToNext()) { - final String type = cursor.getString(0); - final String authToken = cursor.getString(1); - authTokensForAccount.put(type, authToken); - } - } finally { - cursor.close(); - } - return authTokensForAccount; - } + long insertAuthToken(long accountId, String authTokenType, String authToken) { + SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked(); + ContentValues values = new ContentValues(); + values.put(AUTHTOKENS_ACCOUNTS_ID, accountId); + values.put(AUTHTOKENS_TYPE, authTokenType); + values.put(AUTHTOKENS_AUTHTOKEN, authToken); + return db.insert( + CE_TABLE_AUTHTOKENS, AUTHTOKENS_AUTHTOKEN, values); + } - static int updateAccountPassword(SQLiteDatabase db, long accountId, String password) { - final ContentValues values = new ContentValues(); - values.put(ACCOUNTS_PASSWORD, password); - return db.update( - CE_TABLE_ACCOUNTS, values, ACCOUNTS_ID + "=?", - new String[]{String.valueOf(accountId)}); - } + int updateCeAccountPassword(long accountId, String password) { + SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked(); + final ContentValues values = new ContentValues(); + values.put(ACCOUNTS_PASSWORD, password); + return db.update( + CE_TABLE_ACCOUNTS, values, ACCOUNTS_ID + "=?", + new String[] {String.valueOf(accountId)}); + } - static boolean renameAccount(SQLiteDatabase db, long accountId, String newName) { - final ContentValues values = new ContentValues(); - values.put(ACCOUNTS_NAME, newName); - final String[] argsAccountId = {String.valueOf(accountId)}; - return db.update( - CE_TABLE_ACCOUNTS, values, ACCOUNTS_ID + "=?", argsAccountId) > 0; - } + boolean renameCeAccount(long accountId, String newName) { + SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked(); + final ContentValues values = new ContentValues(); + values.put(ACCOUNTS_NAME, newName); + final String[] argsAccountId = {String.valueOf(accountId)}; + return db.update( + CE_TABLE_ACCOUNTS, values, ACCOUNTS_ID + "=?", argsAccountId) > 0; + } - static boolean deleteAuthTokensByAccountId(SQLiteDatabase db, long accountId) { - return db.delete( - CE_TABLE_AUTHTOKENS, AUTHTOKENS_ACCOUNTS_ID + "=?", - new String[]{String.valueOf(accountId)}) > 0; - } + boolean deleteAuthTokensByAccountId(long accountId) { + SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked(); + return db.delete(CE_TABLE_AUTHTOKENS, AUTHTOKENS_ACCOUNTS_ID + "=?", + new String[] {String.valueOf(accountId)}) > 0; + } - static long findExtrasIdByAccountId(SQLiteDatabase db, long accountId, String key) { - Cursor cursor = db.query( - CE_TABLE_EXTRAS, new String[]{EXTRAS_ID}, - EXTRAS_ACCOUNTS_ID + "=" + accountId + " AND " + EXTRAS_KEY + "=?", - new String[]{key}, null, null, null); - try { - if (cursor.moveToNext()) { - return cursor.getLong(0); - } - return -1; - } finally { - cursor.close(); + long findExtrasIdByAccountId(long accountId, String key) { + SQLiteDatabase db = mDeDatabase.getReadableDatabaseUserIsUnlocked(); + Cursor cursor = db.query( + CE_TABLE_EXTRAS, new String[]{EXTRAS_ID}, + EXTRAS_ACCOUNTS_ID + "=" + accountId + " AND " + EXTRAS_KEY + "=?", + new String[]{key}, null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getLong(0); } + return -1; + } finally { + cursor.close(); } + } - static boolean updateExtra(SQLiteDatabase db, long extrasId, String value) { - ContentValues values = new ContentValues(); - values.put(EXTRAS_VALUE, value); - int rows = db.update( - TABLE_EXTRAS, values, EXTRAS_ID + "=?", - new String[]{String.valueOf(extrasId)}); - return rows == 1; - } + boolean updateExtra(long extrasId, String value) { + SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked(); + ContentValues values = new ContentValues(); + values.put(EXTRAS_VALUE, value); + int rows = db.update( + TABLE_EXTRAS, values, EXTRAS_ID + "=?", + new String[]{String.valueOf(extrasId)}); + return rows == 1; + } - static long insertExtra(SQLiteDatabase db, long accountId, String key, String value) { - ContentValues values = new ContentValues(); - values.put(EXTRAS_KEY, key); - values.put(EXTRAS_ACCOUNTS_ID, accountId); - values.put(EXTRAS_VALUE, value); - return db.insert(CE_TABLE_EXTRAS, EXTRAS_KEY, values); - } + long insertExtra(long accountId, String key, String value) { + SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked(); + ContentValues values = new ContentValues(); + values.put(EXTRAS_KEY, key); + values.put(EXTRAS_ACCOUNTS_ID, accountId); + values.put(EXTRAS_VALUE, value); + return db.insert(CE_TABLE_EXTRAS, EXTRAS_KEY, values); + } - static Map<String, String> findUserExtrasForAccount(SQLiteDatabase db, Account account) { - Map<String, String> userExtrasForAccount = new HashMap<>(); - Cursor cursor = db.query(CE_TABLE_EXTRAS, - COLUMNS_EXTRAS_KEY_AND_VALUE, - SELECTION_USERDATA_BY_ACCOUNT, - new String[]{account.name, account.type}, - null, null, null); - try { - while (cursor.moveToNext()) { - final String tmpkey = cursor.getString(0); - final String value = cursor.getString(1); - userExtrasForAccount.put(tmpkey, value); - } - } finally { - cursor.close(); + Map<String, String> findUserExtrasForAccount(Account account) { + SQLiteDatabase db = mDeDatabase.getReadableDatabaseUserIsUnlocked(); + Map<String, String> userExtrasForAccount = new HashMap<>(); + String[] selectionArgs = {account.name, account.type}; + try (Cursor cursor = db.query(CE_TABLE_EXTRAS, + COLUMNS_EXTRAS_KEY_AND_VALUE, + SELECTION_USERDATA_BY_ACCOUNT, + selectionArgs, + null, null, null)) { + while (cursor.moveToNext()) { + final String tmpkey = cursor.getString(0); + final String value = cursor.getString(1); + userExtrasForAccount.put(tmpkey, value); } - return userExtrasForAccount; } + return userExtrasForAccount; + } - static long findAccountId(SQLiteDatabase db, Account account) { - Cursor cursor = db.query( - CE_TABLE_ACCOUNTS, new String[]{ - ACCOUNTS_ID}, - "name=? AND type=?", new String[]{account.name, account.type}, null, null, - null); - try { - if (cursor.moveToNext()) { - return cursor.getLong(0); - } - return -1; - } finally { - cursor.close(); + long findCeAccountId(Account account) { + SQLiteDatabase db = mDeDatabase.getReadableDatabaseUserIsUnlocked(); + String[] columns = { ACCOUNTS_ID }; + String selection = "name=? AND type=?"; + String[] selectionArgs = {account.name, account.type}; + try (Cursor cursor = db.query(CE_TABLE_ACCOUNTS, columns, selection, selectionArgs, + null, null, null)) { + if (cursor.moveToNext()) { + return cursor.getLong(0); } + return -1; } + } - static String findAccountPasswordByNameAndType(SQLiteDatabase db, String name, - String type) { - Cursor cursor = db.query(CE_TABLE_ACCOUNTS, new String[]{ - ACCOUNTS_PASSWORD}, - ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE + "=?", - new String[]{name, type}, null, null, null); - try { - if (cursor.moveToNext()) { - return cursor.getString(0); - } - return null; - } finally { - cursor.close(); + String findAccountPasswordByNameAndType(String name, String type) { + SQLiteDatabase db = mDeDatabase.getReadableDatabaseUserIsUnlocked(); + String selection = ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE + "=?"; + String[] selectionArgs = {name, type}; + String[] columns = {ACCOUNTS_PASSWORD}; + try (Cursor cursor = db.query(CE_TABLE_ACCOUNTS, columns, selection, selectionArgs, + null, null, null)) { + if (cursor.moveToNext()) { + return cursor.getString(0); } + return null; } + } - static long insertAccount(SQLiteDatabase db, Account account, String password) { - ContentValues values = new ContentValues(); - values.put(ACCOUNTS_NAME, account.name); - values.put(ACCOUNTS_TYPE, account.type); - values.put(ACCOUNTS_PASSWORD, password); - return db.insert( - CE_TABLE_ACCOUNTS, ACCOUNTS_NAME, values); - } - + long insertCeAccount(Account account, String password) { + SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked(); + ContentValues values = new ContentValues(); + values.put(ACCOUNTS_NAME, account.name); + values.put(ACCOUNTS_TYPE, account.type); + values.put(ACCOUNTS_PASSWORD, password); + return db.insert( + CE_TABLE_ACCOUNTS, ACCOUNTS_NAME, values); } + static class DeDatabaseHelper extends SQLiteOpenHelper { private final int mUserId; @@ -504,7 +522,7 @@ class AccountsDb { createGrantsTable(db); createSharedAccountsTable(db); createAccountsDeletionTrigger(db); - DebugDbHelper.createDebugTable(db); + createDebugTable(db); } private void createSharedAccountsTable(SQLiteDatabase db) { @@ -533,6 +551,18 @@ class AccountsDb { + "," + GRANTS_GRANTEE_UID + "))"); } + static void createDebugTable(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_DEBUG + " ( " + + ACCOUNTS_ID + " INTEGER," + + DEBUG_TABLE_ACTION_TYPE + " TEXT NOT NULL, " + + DEBUG_TABLE_TIMESTAMP + " DATETIME," + + DEBUG_TABLE_CALLER_UID + " INTEGER NOT NULL," + + DEBUG_TABLE_TABLE_NAME + " TEXT NOT NULL," + + DEBUG_TABLE_KEY + " INTEGER PRIMARY KEY)"); + db.execSQL("CREATE INDEX timestamp_index ON " + TABLE_DEBUG + " (" + + DEBUG_TABLE_TIMESTAMP + ")"); + } + @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.i(TAG, "upgrade from version " + oldVersion + " to version " + newVersion); @@ -542,17 +572,6 @@ class AccountsDb { } } - public void attachCeDatabase(File ceDbFile) { - SQLiteDatabase db = getWritableDatabase(); - db.execSQL("ATTACH DATABASE '" + ceDbFile.getPath()+ "' AS ceDb"); - mCeAttached = true; - } - - public boolean isCeDatabaseAttached() { - return mCeAttached; - } - - public SQLiteDatabase getReadableDatabaseUserIsUnlocked() { if(!mCeAttached) { Log.wtf(TAG, "getReadableDatabaseUserIsUnlocked called while user " + mUserId @@ -616,343 +635,305 @@ class AccountsDb { db.execSQL("DETACH DATABASE preNDb"); } + } - static boolean deleteAccount(SQLiteDatabase db, long accountId) { - return db.delete(TABLE_ACCOUNTS, ACCOUNTS_ID + "=" + accountId, null) > 0; - } + boolean deleteDeAccount(long accountId) { + SQLiteDatabase db = mDeDatabase.getWritableDatabase(); + return db.delete(TABLE_ACCOUNTS, ACCOUNTS_ID + "=" + accountId, null) > 0; + } - static long insertSharedAccount(SQLiteDatabase db, Account account) { - ContentValues values = new ContentValues(); - values.put(ACCOUNTS_NAME, account.name); - values.put(ACCOUNTS_TYPE, account.type); - return db.insert( - TABLE_SHARED_ACCOUNTS, ACCOUNTS_NAME, values); - } + long insertSharedAccount(Account account) { + SQLiteDatabase db = mDeDatabase.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(ACCOUNTS_NAME, account.name); + values.put(ACCOUNTS_TYPE, account.type); + return db.insert( + TABLE_SHARED_ACCOUNTS, ACCOUNTS_NAME, values); + } - static boolean deleteSharedAccount(SQLiteDatabase db, Account account) { - return db - .delete(TABLE_SHARED_ACCOUNTS, ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE + "=?", - new String[]{account.name, account.type}) > 0; - } + boolean deleteSharedAccount(Account account) { + SQLiteDatabase db = mDeDatabase.getWritableDatabase(); + return db.delete(TABLE_SHARED_ACCOUNTS, ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE + "=?", + new String[]{account.name, account.type}) > 0; + } - static int renameSharedAccount(SQLiteDatabase db, Account account, String newName) { - final ContentValues values = new ContentValues(); - values.put(ACCOUNTS_NAME, newName); - return db.update(TABLE_SHARED_ACCOUNTS, - values, - ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE - + "=?", - new String[]{account.name, account.type}); - } + int renameSharedAccount(Account account, String newName) { + SQLiteDatabase db = mDeDatabase.getWritableDatabase(); + final ContentValues values = new ContentValues(); + values.put(ACCOUNTS_NAME, newName); + return db.update(TABLE_SHARED_ACCOUNTS, + values, + ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE + "=?", + new String[] {account.name, account.type}); + } - static List<Account> getSharedAccounts(SQLiteDatabase db) { - ArrayList<Account> accountList = new ArrayList<>(); - Cursor cursor = null; - try { - cursor = db.query(TABLE_SHARED_ACCOUNTS, new String[]{ - ACCOUNTS_NAME, ACCOUNTS_TYPE}, - null, null, null, null, null); - if (cursor != null && cursor.moveToFirst()) { - int nameIndex = cursor.getColumnIndex(ACCOUNTS_NAME); - int typeIndex = cursor.getColumnIndex(ACCOUNTS_TYPE); - do { - accountList.add(new Account(cursor.getString(nameIndex), - cursor.getString(typeIndex))); - } while (cursor.moveToNext()); - } - } finally { - if (cursor != null) { - cursor.close(); - } + List<Account> getSharedAccounts() { + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); + ArrayList<Account> accountList = new ArrayList<>(); + Cursor cursor = null; + try { + cursor = db.query(TABLE_SHARED_ACCOUNTS, new String[] {ACCOUNTS_NAME, ACCOUNTS_TYPE}, + null, null, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + int nameIndex = cursor.getColumnIndex(ACCOUNTS_NAME); + int typeIndex = cursor.getColumnIndex(ACCOUNTS_TYPE); + do { + accountList.add(new Account(cursor.getString(nameIndex), + cursor.getString(typeIndex))); + } while (cursor.moveToNext()); } - return accountList; - } - - static long findSharedAccountId(SQLiteDatabase db, Account account) { - Cursor cursor = db.query(TABLE_SHARED_ACCOUNTS, new String[]{ - ACCOUNTS_ID}, - "name=? AND type=?", new String[]{account.name, account.type}, null, null, - null); - try { - if (cursor.moveToNext()) { - return cursor.getLong(0); - } - return -1; - } finally { + } finally { + if (cursor != null) { cursor.close(); } } + return accountList; + } - static long findAccountLastAuthenticatedTime(SQLiteDatabase db, Account account) { - return DatabaseUtils.longForQuery( - db, - "SELECT " + AccountsDb.ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS - + " FROM " + - TABLE_ACCOUNTS + " WHERE " + ACCOUNTS_NAME + "=? AND " - + ACCOUNTS_TYPE + "=?", - new String[] {account.name, account.type}); + long findSharedAccountId(Account account) { + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); + Cursor cursor = db.query(TABLE_SHARED_ACCOUNTS, new String[]{ + ACCOUNTS_ID}, + "name=? AND type=?", new String[]{account.name, account.type}, null, null, + null); + try { + if (cursor.moveToNext()) { + return cursor.getLong(0); + } + return -1; + } finally { + cursor.close(); } + } - static boolean updateAccountLastAuthenticatedTime(SQLiteDatabase db, Account account) { - final ContentValues values = new ContentValues(); - values.put(ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS, System.currentTimeMillis()); - int rowCount = db.update( - TABLE_ACCOUNTS, - values, - ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE + "=?", - new String[] { - account.name, account.type - }); - return rowCount > 0; - } + long findAccountLastAuthenticatedTime(Account account) { + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); + return DatabaseUtils.longForQuery(db, + "SELECT " + AccountsDb.ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS + + " FROM " + TABLE_ACCOUNTS + " WHERE " + ACCOUNTS_NAME + "=? AND " + + ACCOUNTS_TYPE + "=?", + new String[] {account.name, account.type}); + } + boolean updateAccountLastAuthenticatedTime(Account account) { + SQLiteDatabase db = mDeDatabase.getWritableDatabase(); + final ContentValues values = new ContentValues(); + values.put(ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS, System.currentTimeMillis()); + int rowCount = db.update(TABLE_ACCOUNTS, + values, + ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE + "=?", + new String[] { account.name, account.type }); + return rowCount > 0; + } - static void dumpAccountsTable(SQLiteDatabase db, PrintWriter pw) { - Cursor cursor = db.query( - TABLE_ACCOUNTS, ACCOUNT_TYPE_COUNT_PROJECTION, - null, null, ACCOUNTS_TYPE, null, null); - try { - while (cursor.moveToNext()) { - // print type,count - pw.println(cursor.getString(0) + "," + cursor.getString(1)); - } - } finally { - if (cursor != null) { - cursor.close(); - } + void dumpDeAccountsTable(PrintWriter pw) { + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); + Cursor cursor = db.query( + TABLE_ACCOUNTS, ACCOUNT_TYPE_COUNT_PROJECTION, + null, null, ACCOUNTS_TYPE, null, null); + try { + while (cursor.moveToNext()) { + // print type,count + pw.println(cursor.getString(0) + "," + cursor.getString(1)); + } + } finally { + if (cursor != null) { + cursor.close(); } } + } - static long findAccountId(SQLiteDatabase db, Account account) { - Cursor cursor = db.query( - TABLE_ACCOUNTS, new String[]{ACCOUNTS_ID}, - "name=? AND type=?", new String[]{account.name, account.type}, null, null, - null); - try { - if (cursor.moveToNext()) { - return cursor.getLong(0); - } - return -1; - } finally { - cursor.close(); + long findDeAccountId(Account account) { + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); + String[] columns = {ACCOUNTS_ID}; + String selection = "name=? AND type=?"; + String[] selectionArgs = {account.name, account.type}; + try (Cursor cursor = db.query(TABLE_ACCOUNTS, columns, selection, selectionArgs, + null, null, null)) { + if (cursor.moveToNext()) { + return cursor.getLong(0); } + return -1; } + } - static Map<Long, Account> findAllAccounts(SQLiteDatabase db) { - LinkedHashMap<Long, Account> map = new LinkedHashMap<>(); - Cursor cursor = db.query(TABLE_ACCOUNTS, - new String[]{ACCOUNTS_ID, ACCOUNTS_TYPE, ACCOUNTS_NAME}, - null, null, null, null, ACCOUNTS_ID); - try { - while (cursor.moveToNext()) { - final long accountId = cursor.getLong(0); - final String accountType = cursor.getString(1); - final String accountName = cursor.getString(2); + Map<Long, Account> findAllDeAccounts() { + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); + LinkedHashMap<Long, Account> map = new LinkedHashMap<>(); + String[] columns = {ACCOUNTS_ID, ACCOUNTS_TYPE, ACCOUNTS_NAME}; + try (Cursor cursor = db.query(TABLE_ACCOUNTS, columns, + null, null, null, null, ACCOUNTS_ID)) { + while (cursor.moveToNext()) { + final long accountId = cursor.getLong(0); + final String accountType = cursor.getString(1); + final String accountName = cursor.getString(2); - final Account account = new Account(accountName, accountType); - map.put(accountId, account); - } - } finally { - cursor.close(); + final Account account = new Account(accountName, accountType); + map.put(accountId, account); } - return map; } + return map; + } - static String findAccountPreviousName(SQLiteDatabase db, Account account) { - Cursor cursor = db.query( - TABLE_ACCOUNTS, - new String[]{ACCOUNTS_PREVIOUS_NAME}, - ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE - + "=?", - new String[]{account.name, account.type}, - null, - null, - null); - try { - if (cursor.moveToNext()) { - return cursor.getString(0); - } - } finally { - cursor.close(); + String findDeAccountPreviousName(Account account) { + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); + String[] columns = {ACCOUNTS_PREVIOUS_NAME}; + String selection = ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE + "=?"; + String[] selectionArgs = {account.name, account.type}; + try (Cursor cursor = db.query(TABLE_ACCOUNTS, columns, selection, selectionArgs, + null, null, null)) { + if (cursor.moveToNext()) { + return cursor.getString(0); } - return null; } + return null; + } - static long insertAccount(SQLiteDatabase db, Account account, long accountId) { - ContentValues values = new ContentValues(); - values.put(ACCOUNTS_ID, accountId); - values.put(ACCOUNTS_NAME, account.name); - values.put(ACCOUNTS_TYPE, account.type); - values.put(ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS, System.currentTimeMillis()); - return db.insert(TABLE_ACCOUNTS, ACCOUNTS_NAME, values); - } + long insertDeAccount(Account account, long accountId) { + SQLiteDatabase db = mDeDatabase.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(ACCOUNTS_ID, accountId); + values.put(ACCOUNTS_NAME, account.name); + values.put(ACCOUNTS_TYPE, account.type); + values.put(ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS, System.currentTimeMillis()); + return db.insert(TABLE_ACCOUNTS, ACCOUNTS_NAME, values); + } - static boolean renameAccount(SQLiteDatabase db, long accountId, String newName, - String previousName) { - final ContentValues values = new ContentValues(); - values.put(ACCOUNTS_NAME, newName); - values.put(ACCOUNTS_PREVIOUS_NAME, previousName); - final String[] argsAccountId = {String.valueOf(accountId)}; - return db.update( - TABLE_ACCOUNTS, values, ACCOUNTS_ID + "=?", argsAccountId) > 0; - } + boolean renameDeAccount(long accountId, String newName, String previousName) { + SQLiteDatabase db = mDeDatabase.getWritableDatabase(); + final ContentValues values = new ContentValues(); + values.put(ACCOUNTS_NAME, newName); + values.put(ACCOUNTS_PREVIOUS_NAME, previousName); + final String[] argsAccountId = {String.valueOf(accountId)}; + return db.update(TABLE_ACCOUNTS, values, ACCOUNTS_ID + "=?", argsAccountId) > 0; + } - static boolean deleteGrantsByAccountIdAuthTokenTypeAndUid(SQLiteDatabase db, long accountId, - String authTokenType, long uid) { - return db.delete(TABLE_GRANTS, - GRANTS_ACCOUNTS_ID + "=? AND " + GRANTS_AUTH_TOKEN_TYPE + "=? AND " - + GRANTS_GRANTEE_UID + "=?", - new String[]{String.valueOf(accountId), authTokenType, String.valueOf(uid)}) - > 0; - } + boolean deleteGrantsByAccountIdAuthTokenTypeAndUid(long accountId, + String authTokenType, long uid) { + SQLiteDatabase db = mDeDatabase.getWritableDatabase(); + return db.delete(TABLE_GRANTS, + GRANTS_ACCOUNTS_ID + "=? AND " + GRANTS_AUTH_TOKEN_TYPE + "=? AND " + + GRANTS_GRANTEE_UID + "=?", + new String[] {String.valueOf(accountId), authTokenType, String.valueOf(uid)}) > 0; + } - static List<Integer> findAllUidGrants(SQLiteDatabase db) { - List<Integer> result = new ArrayList<>(); - final Cursor cursor = db.query(TABLE_GRANTS, - new String[]{GRANTS_GRANTEE_UID}, - null, null, GRANTS_GRANTEE_UID, null, null); - try { - while (cursor.moveToNext()) { - final int uid = cursor.getInt(0); - result.add(uid); - } - } finally { - cursor.close(); + List<Integer> findAllUidGrants() { + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); + List<Integer> result = new ArrayList<>(); + final Cursor cursor = db.query(TABLE_GRANTS, + new String[]{GRANTS_GRANTEE_UID}, + null, null, GRANTS_GRANTEE_UID, null, null); + try { + while (cursor.moveToNext()) { + final int uid = cursor.getInt(0); + result.add(uid); } - return result; - } - - static long findMatchingGrantsCount(SQLiteDatabase db, - int uid, String authTokenType, Account account) { - String[] args = {String.valueOf(uid), authTokenType, - account.name, account.type}; - return DatabaseUtils - .longForQuery(db, COUNT_OF_MATCHING_GRANTS, args); + } finally { + cursor.close(); } + return result; + } - static long findMatchingGrantsCountAnyToken(SQLiteDatabase db, - int uid, Account account) { - String[] args = {String.valueOf(uid), account.name, account.type}; - return DatabaseUtils.longForQuery(db, COUNT_OF_MATCHING_GRANTS_ANY_TOKEN, args); - } + long findMatchingGrantsCount(int uid, String authTokenType, Account account) { + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); + String[] args = {String.valueOf(uid), authTokenType, account.name, account.type}; + return DatabaseUtils.longForQuery(db, COUNT_OF_MATCHING_GRANTS, args); + } - static long insertGrant(SQLiteDatabase db, long accountId, String authTokenType, int uid) { - ContentValues values = new ContentValues(); - values.put(GRANTS_ACCOUNTS_ID, accountId); - values.put(GRANTS_AUTH_TOKEN_TYPE, authTokenType); - values.put(GRANTS_GRANTEE_UID, uid); - return db.insert( - TABLE_GRANTS, GRANTS_ACCOUNTS_ID, values); - } + long findMatchingGrantsCountAnyToken(int uid, Account account) { + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); + String[] args = {String.valueOf(uid), account.name, account.type}; + return DatabaseUtils.longForQuery(db, COUNT_OF_MATCHING_GRANTS_ANY_TOKEN, args); + } - static boolean deleteGrantsByUid(SQLiteDatabase db, int uid) { - return db.delete( - TABLE_GRANTS, GRANTS_GRANTEE_UID + "=?", - new String[]{Integer.toString(uid)}) > 0; - } + long insertGrant(long accountId, String authTokenType, int uid) { + SQLiteDatabase db = mDeDatabase.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(GRANTS_ACCOUNTS_ID, accountId); + values.put(GRANTS_AUTH_TOKEN_TYPE, authTokenType); + values.put(GRANTS_GRANTEE_UID, uid); + return db.insert(TABLE_GRANTS, GRANTS_ACCOUNTS_ID, values); + } - static long insertMetaAuthTypeAndUid(SQLiteDatabase db, String authenticatorType, int uid) { - ContentValues values = new ContentValues(); - values.put(META_KEY, - META_KEY_FOR_AUTHENTICATOR_UID_FOR_TYPE_PREFIX + authenticatorType); - values.put(META_VALUE, uid); - return db.insert(TABLE_META, null, values); - } + boolean deleteGrantsByUid(int uid) { + SQLiteDatabase db = mDeDatabase.getWritableDatabase(); + return db.delete(TABLE_GRANTS, GRANTS_GRANTEE_UID + "=?", + new String[] {Integer.toString(uid)}) > 0; + } - static long insertOrReplaceMetaAuthTypeAndUid(SQLiteDatabase db, String authenticatorType, - int uid) { - ContentValues values = new ContentValues(); - values.put(META_KEY, - META_KEY_FOR_AUTHENTICATOR_UID_FOR_TYPE_PREFIX + authenticatorType); - values.put(META_VALUE, uid); - return db.insertWithOnConflict(TABLE_META, null, values, - SQLiteDatabase.CONFLICT_REPLACE); - } + long insertOrReplaceMetaAuthTypeAndUid(String authenticatorType, int uid) { + SQLiteDatabase db = mDeDatabase.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(META_KEY, + META_KEY_FOR_AUTHENTICATOR_UID_FOR_TYPE_PREFIX + authenticatorType); + values.put(META_VALUE, uid); + return db.insertWithOnConflict(TABLE_META, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + } - static Map<String, Integer> findMetaAuthUid(SQLiteDatabase db) { - Cursor metaCursor = db.query( - TABLE_META, - new String[]{META_KEY, META_VALUE}, - SELECTION_META_BY_AUTHENTICATOR_TYPE, - new String[]{META_KEY_FOR_AUTHENTICATOR_UID_FOR_TYPE_PREFIX + "%"}, - null /* groupBy */, - null /* having */, - META_KEY); - Map<String, Integer> map = new LinkedHashMap<>(); - try { - while (metaCursor.moveToNext()) { - String type = TextUtils - .split(metaCursor.getString(0), META_KEY_DELIMITER)[1]; - String uidStr = metaCursor.getString(1); - if (TextUtils.isEmpty(type) || TextUtils.isEmpty(uidStr)) { - // Should never happen. - Slog.e(TAG, "Auth type empty: " + TextUtils.isEmpty(type) - + ", uid empty: " + TextUtils.isEmpty(uidStr)); - continue; - } - int uid = Integer.parseInt(metaCursor.getString(1)); - map.put(type, uid); + Map<String, Integer> findMetaAuthUid() { + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); + Cursor metaCursor = db.query( + TABLE_META, + new String[]{META_KEY, META_VALUE}, + SELECTION_META_BY_AUTHENTICATOR_TYPE, + new String[]{META_KEY_FOR_AUTHENTICATOR_UID_FOR_TYPE_PREFIX + "%"}, + null /* groupBy */, + null /* having */, + META_KEY); + Map<String, Integer> map = new LinkedHashMap<>(); + try { + while (metaCursor.moveToNext()) { + String type = TextUtils + .split(metaCursor.getString(0), META_KEY_DELIMITER)[1]; + String uidStr = metaCursor.getString(1); + if (TextUtils.isEmpty(type) || TextUtils.isEmpty(uidStr)) { + // Should never happen. + Slog.e(TAG, "Auth type empty: " + TextUtils.isEmpty(type) + + ", uid empty: " + TextUtils.isEmpty(uidStr)); + continue; } - } finally { - metaCursor.close(); + int uid = Integer.parseInt(metaCursor.getString(1)); + map.put(type, uid); } - return map; - } - - static boolean deleteMetaByAuthTypeAndUid(SQLiteDatabase db, String type, int uid) { - return db.delete( - TABLE_META, - META_KEY + "=? AND " + META_VALUE + "=?", - new String[]{ - META_KEY_FOR_AUTHENTICATOR_UID_FOR_TYPE_PREFIX + type, - String.valueOf(uid)} - ) > 0; + } finally { + metaCursor.close(); } + return map; + } - static List<Pair<String, Integer>> findAllAccountGrants(SQLiteDatabase db) { - try (Cursor cursor = db.rawQuery(ACCOUNT_ACCESS_GRANTS, null)) { - if (cursor == null || !cursor.moveToFirst()) { - return Collections.emptyList(); - } - List<Pair<String, Integer>> results = new ArrayList<>(); - do { - final String accountName = cursor.getString(0); - final int uid = cursor.getInt(1); - results.add(Pair.create(accountName, uid)); - } while (cursor.moveToNext()); - return results; - } - } + boolean deleteMetaByAuthTypeAndUid(String type, int uid) { + SQLiteDatabase db = mDeDatabase.getWritableDatabase(); + return db.delete( + TABLE_META, + META_KEY + "=? AND " + META_VALUE + "=?", + new String[]{ + META_KEY_FOR_AUTHENTICATOR_UID_FOR_TYPE_PREFIX + type, + String.valueOf(uid)} + ) > 0; + } - static DeDatabaseHelper create( - Context context, - int userId, - File preNDatabaseFile, - File deDatabaseFile) { - boolean newDbExists = deDatabaseFile.exists(); - DeDatabaseHelper deDatabaseHelper = new DeDatabaseHelper(context, userId, - deDatabaseFile.getPath()); - // If the db just created, and there is a legacy db, migrate it - if (!newDbExists && preNDatabaseFile.exists()) { - // Migrate legacy db to the latest version - PRE_N_DATABASE_VERSION - PreNDatabaseHelper - preNDatabaseHelper = new PreNDatabaseHelper(context, userId, - preNDatabaseFile.getPath()); - // Open the database to force upgrade if required - preNDatabaseHelper.getWritableDatabase(); - preNDatabaseHelper.close(); - // Move data without SPII to DE - deDatabaseHelper.migratePreNDbToDe(preNDatabaseFile); + List<Pair<String, Integer>> findAllAccountGrants() { + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); + try (Cursor cursor = db.rawQuery(ACCOUNT_ACCESS_GRANTS, null)) { + if (cursor == null || !cursor.moveToFirst()) { + return Collections.emptyList(); } - return deDatabaseHelper; + List<Pair<String, Integer>> results = new ArrayList<>(); + do { + final String accountName = cursor.getString(0); + final int uid = cursor.getInt(1); + results.add(Pair.create(accountName, uid)); + } while (cursor.moveToNext()); + return results; } } - static class PreNDatabaseHelper extends SQLiteOpenHelper { + private static class PreNDatabaseHelper extends SQLiteOpenHelper { private final Context mContext; private final int mUserId; - public PreNDatabaseHelper(Context context, int userId, String preNDatabaseName) { + PreNDatabaseHelper(Context context, int userId, String preNDatabaseName) { super(context, preNDatabaseName, null, PRE_N_DATABASE_VERSION); mContext = context; mUserId = userId; @@ -982,7 +963,7 @@ class AccountsDb { } private void addDebugTable(SQLiteDatabase db) { - DebugDbHelper.createDebugTable(db); + DeDatabaseHelper.createDebugTable(db); } private void createAccountsDeletionTrigger(SQLiteDatabase db) { @@ -1007,10 +988,18 @@ class AccountsDb { + "," + GRANTS_GRANTEE_UID + "))"); } + static long insertMetaAuthTypeAndUid(SQLiteDatabase db, String authenticatorType, int uid) { + ContentValues values = new ContentValues(); + values.put(META_KEY, + META_KEY_FOR_AUTHENTICATOR_UID_FOR_TYPE_PREFIX + authenticatorType); + values.put(META_VALUE, uid); + return db.insert(TABLE_META, null, values); + } + private void populateMetaTableWithAuthTypeAndUID(SQLiteDatabase db, Map<String, Integer> authTypeAndUIDMap) { for (Map.Entry<String, Integer> entry : authTypeAndUIDMap.entrySet()) { - DeDatabaseHelper.insertMetaAuthTypeAndUid(db, entry.getKey(), entry.getValue()); + insertMetaAuthTypeAndUid(db, entry.getKey(), entry.getValue()); } } @@ -1078,68 +1067,8 @@ class AccountsDb { } } - static class DebugDbHelper{ - private DebugDbHelper() { - } - - - private static void createDebugTable(SQLiteDatabase db) { - db.execSQL("CREATE TABLE " + TABLE_DEBUG + " ( " - + ACCOUNTS_ID + " INTEGER," - + DEBUG_TABLE_ACTION_TYPE + " TEXT NOT NULL, " - + DEBUG_TABLE_TIMESTAMP + " DATETIME," - + DEBUG_TABLE_CALLER_UID + " INTEGER NOT NULL," - + DEBUG_TABLE_TABLE_NAME + " TEXT NOT NULL," - + DEBUG_TABLE_KEY + " INTEGER PRIMARY KEY)"); - db.execSQL("CREATE INDEX timestamp_index ON " + TABLE_DEBUG + " (" - + DEBUG_TABLE_TIMESTAMP + ")"); - } - - static SQLiteStatement compileSqlStatementForLogging(SQLiteDatabase db) { - String sql = "INSERT OR REPLACE INTO " + AccountsDb.TABLE_DEBUG - + " VALUES (?,?,?,?,?,?)"; - return db.compileStatement(sql); - } - - static int getDebugTableRowCount(SQLiteDatabase db) { - String queryCountDebugDbRows = "SELECT COUNT(*) FROM " + TABLE_DEBUG; - return (int) DatabaseUtils.longForQuery(db, queryCountDebugDbRows, null); - } - - /* - * Finds the row key where the next insertion should take place. This should - * be invoked only if the table has reached its full capacity. - */ - static int getDebugTableInsertionPoint(SQLiteDatabase db) { - // This query finds the smallest timestamp value (and if 2 records have - // same timestamp, the choose the lower id). - String queryCountDebugDbRows = "SELECT " + DEBUG_TABLE_KEY + - " FROM " + TABLE_DEBUG + - " ORDER BY " + DEBUG_TABLE_TIMESTAMP + "," + DEBUG_TABLE_KEY + - " LIMIT 1"; - return (int) DatabaseUtils.longForQuery(db, queryCountDebugDbRows, null); - } - - static void dumpDebugTable(SQLiteDatabase db, PrintWriter pw) { - Cursor cursor = db.query(TABLE_DEBUG, null, - null, null, null, null, DEBUG_TABLE_TIMESTAMP); - pw.println("AccountId, Action_Type, timestamp, UID, TableName, Key"); - pw.println("Accounts History"); - try { - while (cursor.moveToNext()) { - // print type,count - pw.println(cursor.getString(0) + "," + cursor.getString(1) + "," + - cursor.getString(2) + "," + cursor.getString(3) + "," - + cursor.getString(4) + "," + cursor.getString(5)); - } - } finally { - cursor.close(); - } - } - - } - - static List<Account> findCeAccountsNotInDe(SQLiteDatabase db) { + List<Account> findCeAccountsNotInDe() { + SQLiteDatabase db = mDeDatabase.getReadableDatabaseUserIsUnlocked(); // Select accounts from CE that do not exist in DE Cursor cursor = db.rawQuery( "SELECT " + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE @@ -1161,14 +1090,110 @@ class AccountsDb { } } - static boolean deleteCeAccount(SQLiteDatabase db, long accountId) { + boolean deleteCeAccount(long accountId) { + SQLiteDatabase db = mDeDatabase.getReadableDatabaseUserIsUnlocked(); return db.delete( CE_TABLE_ACCOUNTS, ACCOUNTS_ID + "=" + accountId, null) > 0; } + boolean isCeDatabaseAttached() { + return mDeDatabase.mCeAttached; + } + + void beginTransaction() { + mDeDatabase.getWritableDatabase().beginTransaction(); + } + + void setTransactionSuccessful() { + mDeDatabase.getWritableDatabase().setTransactionSuccessful(); + } + + void endTransaction() { + mDeDatabase.getWritableDatabase().endTransaction(); + } + + void attachCeDatabase(File ceDbFile) { + CeDatabaseHelper.create(mContext, mPreNDatabaseFile, ceDbFile); + SQLiteDatabase db = mDeDatabase.getWritableDatabase(); + db.execSQL("ATTACH DATABASE '" + ceDbFile.getPath()+ "' AS ceDb"); + mDeDatabase.mCeAttached = true; + } + + /* + * Finds the row key where the next insertion should take place. Returns number of rows + * if it is less {@link #MAX_DEBUG_DB_SIZE}, otherwise finds the lowest number available. + */ + int calculateDebugTableInsertionPoint() { + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); + String queryCountDebugDbRows = "SELECT COUNT(*) FROM " + TABLE_DEBUG; + int size = (int) DatabaseUtils.longForQuery(db, queryCountDebugDbRows, null); + if (size < MAX_DEBUG_DB_SIZE) { + return size; + } + + // This query finds the smallest timestamp value (and if 2 records have + // same timestamp, the choose the lower id). + queryCountDebugDbRows = "SELECT " + DEBUG_TABLE_KEY + + " FROM " + TABLE_DEBUG + + " ORDER BY " + DEBUG_TABLE_TIMESTAMP + "," + DEBUG_TABLE_KEY + + " LIMIT 1"; + return (int) DatabaseUtils.longForQuery(db, queryCountDebugDbRows, null); + } + + SQLiteStatement compileSqlStatementForLogging() { + // TODO b/31708085 Fix debug logging - it eagerly opens database for write without a need + SQLiteDatabase db = mDeDatabase.getWritableDatabase(); + String sql = "INSERT OR REPLACE INTO " + AccountsDb.TABLE_DEBUG + + " VALUES (?,?,?,?,?,?)"; + return db.compileStatement(sql); + } + + void dumpDebugTable(PrintWriter pw) { + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); + Cursor cursor = db.query(TABLE_DEBUG, null, + null, null, null, null, DEBUG_TABLE_TIMESTAMP); + pw.println("AccountId, Action_Type, timestamp, UID, TableName, Key"); + pw.println("Accounts History"); + try { + while (cursor.moveToNext()) { + // print type,count + pw.println(cursor.getString(0) + "," + cursor.getString(1) + "," + + cursor.getString(2) + "," + cursor.getString(3) + "," + + cursor.getString(4) + "," + cursor.getString(5)); + } + } finally { + cursor.close(); + } + } + + public void close() { + mDeDatabase.close(); + } + static void deleteDbFileWarnIfFailed(File dbFile) { if (!SQLiteDatabase.deleteDatabase(dbFile)) { Log.w(TAG, "Database at " + dbFile + " was not deleted successfully"); } } + + public static AccountsDb create(Context context, int userId, File preNDatabaseFile, + File deDatabaseFile) { + boolean newDbExists = deDatabaseFile.exists(); + DeDatabaseHelper deDatabaseHelper = new DeDatabaseHelper(context, userId, + deDatabaseFile.getPath()); + // If the db just created, and there is a legacy db, migrate it + if (!newDbExists && preNDatabaseFile.exists()) { + // Migrate legacy db to the latest version - PRE_N_DATABASE_VERSION + PreNDatabaseHelper + preNDatabaseHelper = new PreNDatabaseHelper(context, userId, + preNDatabaseFile.getPath()); + // Open the database to force upgrade if required + preNDatabaseHelper.getWritableDatabase(); + preNDatabaseHelper.close(); + // Move data without SPII to DE + deDatabaseHelper.migratePreNDbToDe(preNDatabaseFile); + } + return new AccountsDb(deDatabaseHelper, context, preNDatabaseFile); + } + } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index a8274d72eada..c32cac8a1b37 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -182,6 +182,7 @@ import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; +import android.os.ShellCallback; import android.os.StrictMode; import android.os.SystemClock; import android.os.SystemProperties; @@ -6126,6 +6127,8 @@ public final class ActivityManagerService extends ActivityManagerNative ProcessList.INVALID_ADJ, callerWillRestart, true, doit, evenPersistent, packageName == null ? ("stop user " + userId) : ("stop " + packageName)); + didSomething |= mActivityStarter.clearPendingActivityLaunchesLocked(packageName); + if (mStackSupervisor.finishDisabledPackageActivitiesLocked( packageName, null, doit, evenPersistent, userId)) { if (!doit) { @@ -11472,6 +11475,7 @@ public final class ActivityManagerService extends ActivityManagerNative && userId == UserHandle.USER_SYSTEM && (info.flags & PERSISTENT_MASK) == PERSISTENT_MASK) { r.persistent = true; + r.maxAdj = ProcessList.PERSISTENT_PROC_ADJ; } addProcessNameLocked(r); return r; @@ -14023,9 +14027,10 @@ public final class ActivityManagerService extends ActivityManagerNative @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, - FileDescriptor err, String[] args, ResultReceiver resultReceiver) { + FileDescriptor err, String[] args, ShellCallback callback, + ResultReceiver resultReceiver) { (new ActivityManagerShellCommand(this, false)).exec( - this, in, out, err, args, resultReceiver); + this, in, out, err, args, callback, resultReceiver); } @Override @@ -14248,7 +14253,8 @@ public final class ActivityManagerService extends ActivityManagerNative // Dumping a single activity? if (!dumpActivity(fd, pw, cmd, args, opti, dumpAll, dumpVisibleStacks)) { ActivityManagerShellCommand shell = new ActivityManagerShellCommand(this, true); - int res = shell.exec(this, null, fd, null, args, new ResultReceiver(null)); + int res = shell.exec(this, null, fd, null, args, null, + new ResultReceiver(null)); if (res < 0) { pw.println("Bad activity command, or no activities match: " + cmd); pw.println("Use -h for help."); @@ -20678,8 +20684,11 @@ public final class ActivityManagerService extends ActivityManagerNative final long now = SystemClock.elapsedRealtime(); Long lastReported = userState.mProviderLastReportedFg.get(authority); if (lastReported == null || lastReported < now - 60 * 1000L) { - mUsageStatsService.reportContentProviderUsage( - authority, providerPkgName, app.userId); + if (mSystemReady) { + // Cannot touch the user stats if not system ready + mUsageStatsService.reportContentProviderUsage( + authority, providerPkgName, app.userId); + } userState.mProviderLastReportedFg.put(authority, now); } } @@ -21696,6 +21705,13 @@ public final class ActivityManagerService extends ActivityManagerNative return mUserController.getCurrentUser(); } + String getStartedUserState(int userId) { + synchronized (this) { + final UserState userState = mUserController.getStartedUserStateLocked(userId); + return UserState.stateToString(userState.state); + } + } + @Override public boolean isUserRunning(int userId, int flags) { if (!mUserController.isSameProfileGroup(userId, UserHandle.getCallingUserId()) diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index adf6d3670af3..7142d3fa5c2e 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -17,26 +17,57 @@ package com.android.server.am; import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.AppGlobals; import android.app.IActivityManager; +import android.app.ProfilerInfo; +import android.content.Intent; +import android.content.pm.IPackageManager; +import android.content.pm.ResolveInfo; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ShellCommand; +import android.os.SystemClock; import android.os.UserHandle; import android.util.DebugUtils; import java.io.PrintWriter; +import java.net.URISyntaxException; +import java.util.List; + +import static android.app.ActivityManager.StackId.INVALID_STACK_ID; class ActivityManagerShellCommand extends ShellCommand { + public static final String NO_CLASS_ERROR_CODE = "Error type 3"; + // IPC interface to activity manager -- don't need to do additional security checks. final IActivityManager mInterface; // Internal service impl -- must perform security checks before touching. final ActivityManagerService mInternal; + // Convenience for interacting with package manager. + final IPackageManager mPm; + + private int mStartFlags = 0; + private boolean mWaitOption = false; + private boolean mStopOption = false; + + private int mRepeat = 0; + private int mUserId; + private String mReceiverPermission; + + private String mProfileFile; + private int mSamplingInterval; + private boolean mAutoStop; + private int mStackId; + final boolean mDumping; ActivityManagerShellCommand(ActivityManagerService service, boolean dumping) { mInterface = service; mInternal = service; + mPm = AppGlobals.getPackageManager(); mDumping = dumping; } @@ -48,6 +79,15 @@ class ActivityManagerShellCommand extends ShellCommand { PrintWriter pw = getOutPrintWriter(); try { switch (cmd) { + case "start": + case "start-activity": + return runStartActivity(pw); + case "startservice": + case "start-service": + return 1; //runStartService(pw); + case "stopservice": + case "stop-service": + return 1; //runStopService(pw); case "force-stop": return runForceStop(pw); case "kill": @@ -66,6 +106,8 @@ class ActivityManagerShellCommand extends ShellCommand { return runLenientBackgroundCheck(pw); case "get-uid-state": return getUidState(pw); + case "get-started-user-state": + return getStartedUserState(pw); default: return handleDefaultCommands(cmd); } @@ -75,6 +117,241 @@ class ActivityManagerShellCommand extends ShellCommand { return -1; } + private Intent makeIntent(int defUser) throws URISyntaxException { + mStartFlags = 0; + mWaitOption = false; + mStopOption = false; + mRepeat = 0; + mProfileFile = null; + mSamplingInterval = 0; + mAutoStop = false; + mUserId = defUser; + mStackId = INVALID_STACK_ID; + + return Intent.parseCommandArgs(this, new Intent.CommandOptionHandler() { + @Override + public boolean handleOption(String opt, ShellCommand cmd) { + if (opt.equals("-D")) { + mStartFlags |= ActivityManager.START_FLAG_DEBUG; + } else if (opt.equals("-N")) { + mStartFlags |= ActivityManager.START_FLAG_NATIVE_DEBUGGING; + } else if (opt.equals("-W")) { + mWaitOption = true; + } else if (opt.equals("-P")) { + mProfileFile = getNextArgRequired(); + mAutoStop = true; + } else if (opt.equals("--start-profiler")) { + mProfileFile = getNextArgRequired(); + mAutoStop = false; + } else if (opt.equals("--sampling")) { + mSamplingInterval = Integer.parseInt(getNextArgRequired()); + } else if (opt.equals("-R")) { + mRepeat = Integer.parseInt(getNextArgRequired()); + } else if (opt.equals("-S")) { + mStopOption = true; + } else if (opt.equals("--track-allocation")) { + mStartFlags |= ActivityManager.START_FLAG_TRACK_ALLOCATION; + } else if (opt.equals("--user")) { + mUserId = UserHandle.parseUserArg(getNextArgRequired()); + } else if (opt.equals("--receiver-permission")) { + mReceiverPermission = getNextArgRequired(); + } else if (opt.equals("--stack")) { + mStackId = Integer.parseInt(getNextArgRequired()); + } else { + return false; + } + return true; + } + }); + } + + ParcelFileDescriptor openOutputFile(String path) { + try { + ParcelFileDescriptor pfd = getShellCallback().openOutputFile(path, + "u:r:system_server:s0"); + if (pfd != null) { + return pfd; + } + } catch (RuntimeException e) { + getErrPrintWriter().println("Failure opening file: " + e.getMessage()); + } + getErrPrintWriter().println("Error: Unable to open file: " + path); + getErrPrintWriter().println("Consider using a file under /data/local/tmp/"); + return null; + } + + int runStartActivity(PrintWriter pw) throws RemoteException { + Intent intent; + try { + intent = makeIntent(UserHandle.USER_CURRENT); + } catch (URISyntaxException e) { + throw new RuntimeException(e.getMessage(), e); + } + + if (mUserId == UserHandle.USER_ALL) { + getErrPrintWriter().println("Error: Can't start service with user 'all'"); + return 1; + } + + String mimeType = intent.getType(); + if (mimeType == null && intent.getData() != null + && "content".equals(intent.getData().getScheme())) { + mimeType = mInterface.getProviderMimeType(intent.getData(), mUserId); + } + + do { + if (mStopOption) { + String packageName; + if (intent.getComponent() != null) { + packageName = intent.getComponent().getPackageName(); + } else { + List<ResolveInfo> activities = mPm.queryIntentActivities(intent, mimeType, 0, + mUserId).getList(); + if (activities == null || activities.size() <= 0) { + getErrPrintWriter().println("Error: Intent does not match any activities: " + + intent); + return 1; + } else if (activities.size() > 1) { + getErrPrintWriter().println( + "Error: Intent matches multiple activities; can't stop: " + + intent); + return 1; + } + packageName = activities.get(0).activityInfo.packageName; + } + pw.println("Stopping: " + packageName); + mInterface.forceStopPackage(packageName, mUserId); + try { + Thread.sleep(250); + } catch (InterruptedException e) { + } + } + + ProfilerInfo profilerInfo = null; + + if (mProfileFile != null) { + ParcelFileDescriptor fd = openOutputFile(mProfileFile); + if (fd == null) { + return 1; + } + profilerInfo = new ProfilerInfo(mProfileFile, fd, mSamplingInterval, mAutoStop); + } + + pw.println("Starting: " + intent); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + IActivityManager.WaitResult result = null; + int res; + final long startTime = SystemClock.uptimeMillis(); + ActivityOptions options = null; + if (mStackId != INVALID_STACK_ID) { + options = ActivityOptions.makeBasic(); + options.setLaunchStackId(mStackId); + } + if (mWaitOption) { + result = mInterface.startActivityAndWait(null, null, intent, mimeType, + null, null, 0, mStartFlags, profilerInfo, + options != null ? options.toBundle() : null, mUserId); + res = result.result; + } else { + res = mInterface.startActivityAsUser(null, null, intent, mimeType, + null, null, 0, mStartFlags, profilerInfo, + options != null ? options.toBundle() : null, mUserId); + } + final long endTime = SystemClock.uptimeMillis(); + PrintWriter out = mWaitOption ? pw : getErrPrintWriter(); + boolean launched = false; + switch (res) { + case ActivityManager.START_SUCCESS: + launched = true; + break; + case ActivityManager.START_SWITCHES_CANCELED: + launched = true; + out.println( + "Warning: Activity not started because the " + + " current activity is being kept for the user."); + break; + case ActivityManager.START_DELIVERED_TO_TOP: + launched = true; + out.println( + "Warning: Activity not started, intent has " + + "been delivered to currently running " + + "top-most instance."); + break; + case ActivityManager.START_RETURN_INTENT_TO_CALLER: + launched = true; + out.println( + "Warning: Activity not started because intent " + + "should be handled by the caller"); + break; + case ActivityManager.START_TASK_TO_FRONT: + launched = true; + out.println( + "Warning: Activity not started, its current " + + "task has been brought to the front"); + break; + case ActivityManager.START_INTENT_NOT_RESOLVED: + out.println( + "Error: Activity not started, unable to " + + "resolve " + intent.toString()); + break; + case ActivityManager.START_CLASS_NOT_FOUND: + out.println(NO_CLASS_ERROR_CODE); + out.println("Error: Activity class " + + intent.getComponent().toShortString() + + " does not exist."); + break; + case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT: + out.println( + "Error: Activity not started, you requested to " + + "both forward and receive its result"); + break; + case ActivityManager.START_PERMISSION_DENIED: + out.println( + "Error: Activity not started, you do not " + + "have permission to access it."); + break; + case ActivityManager.START_NOT_VOICE_COMPATIBLE: + out.println( + "Error: Activity not started, voice control not allowed for: " + + intent); + break; + case ActivityManager.START_NOT_CURRENT_USER_ACTIVITY: + out.println( + "Error: Not allowed to start background user activity" + + " that shouldn't be displayed for all users."); + break; + default: + out.println( + "Error: Activity not started, unknown error code " + res); + break; + } + if (mWaitOption && launched) { + if (result == null) { + result = new IActivityManager.WaitResult(); + result.who = intent.getComponent(); + } + pw.println("Status: " + (result.timeout ? "timeout" : "ok")); + if (result.who != null) { + pw.println("Activity: " + result.who.flattenToShortString()); + } + if (result.thisTime >= 0) { + pw.println("ThisTime: " + result.thisTime); + } + if (result.totalTime >= 0) { + pw.println("TotalTime: " + result.totalTime); + } + pw.println("WaitTime: " + (endTime-startTime)); + pw.println("Complete"); + } + mRepeat--; + if (mRepeat > 0) { + mInterface.unhandledBack(); + } + } while (mRepeat > 0); + return 0; + } + int runIsUserStopped(PrintWriter pw) { int userId = UserHandle.parseUserArg(getNextArgRequired()); boolean stopped = mInternal.isUserStopped(userId); @@ -183,6 +460,18 @@ class ActivityManagerShellCommand extends ShellCommand { return 0; } + int getStartedUserState(PrintWriter pw) throws RemoteException { + mInternal.enforceCallingPermission(android.Manifest.permission.DUMP, + "getStartedUserState()"); + final int userId = Integer.parseInt(getNextArgRequired()); + try { + pw.println(mInternal.getStartedUserState(userId)); + } catch (NullPointerException e) { + pw.println("User is not started: " + userId); + } + return 0; + } + @Override public void onHelp() { PrintWriter pw = getOutPrintWriter(); @@ -223,6 +512,24 @@ class ActivityManagerShellCommand extends ShellCommand { pw.println("Activity manager (activity) commands:"); pw.println(" help"); pw.println(" Print this help text."); + pw.println(" start-activity [-D] [-N] [-W] [-P <FILE>] [--start-profiler <FILE>]"); + pw.println(" [--sampling INTERVAL] [-R COUNT] [-S]"); + pw.println(" [--track-allocation] [--user <USER_ID> | current] <INTENT>"); + pw.println(" Start an Activity. Options are:"); + pw.println(" -D: enable debugging"); + pw.println(" -N: enable native debugging"); + pw.println(" -W: wait for launch to complete"); + pw.println(" --start-profiler <FILE>: start profiler and send results to <FILE>"); + pw.println(" --sampling INTERVAL: use sample profiling with INTERVAL microseconds"); + pw.println(" between samples (use with --start-profiler)"); + pw.println(" -P <FILE>: like above, but profiling stops when app goes idle"); + pw.println(" -R: repeat the activity launch <COUNT> times. Prior to each repeat,"); + pw.println(" the top activity will be finished."); + pw.println(" -S: force stop the target app before starting the activity"); + pw.println(" --track-allocation: enable tracking of object allocations"); + pw.println(" --user <USER_ID> | current: Specify which user to run as; if not"); + pw.println(" specified then run as the current user."); + pw.println(" --stack <STACK_ID>: Specify into which stack should the activity be put."); pw.println(" force-stop [--user <USER_ID> | all | current] <PACKAGE>"); pw.println(" Completely stop the given application package."); pw.println(" kill [--user <USER_ID> | all | current] <PACKAGE>"); @@ -241,6 +548,10 @@ class ActivityManagerShellCommand extends ShellCommand { pw.println(" Optionally controls lenient background check mode, returns current mode."); pw.println(" get-uid-state <UID>"); pw.println(" Gets the process state of an app given its <UID>."); + pw.println(" get-started-user-state <USER_ID>"); + pw.println(" Gets the current state of the given started user."); + pw.println(); + Intent.printIntentArgsHelp(pw, ""); } } } diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java index 7708b0205074..d8206276f31a 100644 --- a/services/core/java/com/android/server/am/ActivityStack.java +++ b/services/core/java/com/android/server/am/ActivityStack.java @@ -1414,7 +1414,12 @@ final class ActivityStack { * this function updates the rest of our state to match that fact. */ private void completeResumeLocked(ActivityRecord next) { + boolean wasVisible = next.visible; next.visible = true; + if (!wasVisible) { + // Visibility has changed, so take a note of it so we call the TaskStackChangedListener + mStackSupervisor.mAppVisibilitiesChangedSinceLastPause = true; + } next.idle = false; next.results = null; next.newIntents = null; @@ -5029,7 +5034,7 @@ final class ActivityStack { if (top >= 0) { final ArrayList<ActivityRecord> activities = mTaskHistory.get(top).mActivities; int activityTop = activities.size() - 1; - if (activityTop > 0) { + if (activityTop >= 0) { finishActivityLocked(activities.get(activityTop), Activity.RESULT_CANCELED, null, "unhandled-back", true); } diff --git a/services/core/java/com/android/server/am/ActivityStarter.java b/services/core/java/com/android/server/am/ActivityStarter.java index 5feaf1fb98de..028c6ac54320 100644 --- a/services/core/java/com/android/server/am/ActivityStarter.java +++ b/services/core/java/com/android/server/am/ActivityStarter.java @@ -2091,4 +2091,18 @@ class ActivityStarter { return (flags & Intent.FLAG_ACTIVITY_NEW_DOCUMENT) != 0 && (flags & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) == 0; } + + boolean clearPendingActivityLaunchesLocked(String packageName) { + boolean didSomething = false; + + for (int palNdx = mPendingActivityLaunches.size() - 1; palNdx >= 0; --palNdx) { + PendingActivityLaunch pal = mPendingActivityLaunches.get(palNdx); + ActivityRecord r = pal.r; + if (r != null && r.packageName.equals(packageName)) { + mPendingActivityLaunches.remove(palNdx); + didSomething = true; + } + } + return didSomething; + } } diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index ba42d3f7f29a..96978551d56e 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -115,7 +115,7 @@ final class UserController { // Amount of time we wait for observers to handle a user switch before // giving up on them and unfreezing the screen. - static final int USER_SWITCH_TIMEOUT = 2 * 1000; + static final int USER_SWITCH_TIMEOUT = 3 * 1000; private final Object mLock; private final Injector mInjector; @@ -1103,6 +1103,7 @@ final class UserController { mCurWaitingUserSwitchCallbacks = curWaitingUserSwitchCallbacks; } final AtomicInteger waitingCallbacksCount = new AtomicInteger(observerCount); + final long dispatchStartedTime = SystemClock.elapsedRealtime(); for (int i = 0; i < observerCount; i++) { try { // Prepend with unique prefix to guarantee that keys are unique @@ -1114,6 +1115,11 @@ final class UserController { @Override public void sendResult(Bundle data) throws RemoteException { synchronized (mLock) { + long delay = SystemClock.elapsedRealtime() - dispatchStartedTime; + if (delay > USER_SWITCH_TIMEOUT) { + Slog.wtf(TAG, "User switch timeout: observer " + name + + " sent result after " + delay + " ms"); + } // Early return if this session is no longer valid if (curWaitingUserSwitchCallbacks != mCurWaitingUserSwitchCallbacks) { diff --git a/services/core/java/com/android/server/am/UserState.java b/services/core/java/com/android/server/am/UserState.java index ff8014cbb985..48238b6f4cf3 100644 --- a/services/core/java/com/android/server/am/UserState.java +++ b/services/core/java/com/android/server/am/UserState.java @@ -87,7 +87,7 @@ public final class UserState { state = newState; } - private static String stateToString(int state) { + static String stateToString(int state) { switch (state) { case STATE_BOOTING: return "BOOTING"; case STATE_RUNNING_LOCKED: return "RUNNING_LOCKED"; diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 96ba25955dce..0f351f62d991 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -209,6 +209,7 @@ public class AudioService extends IAudioService.Stub { private static final int MSG_BT_HEADSET_CNCT_FAILED = 9; private static final int MSG_SET_ALL_VOLUMES = 10; private static final int MSG_REPORT_NEW_ROUTES = 12; + private static final int MSG_SET_FORCE_BT_A2DP_USE = 13; private static final int MSG_CHECK_MUSIC_ACTIVE = 14; private static final int MSG_BROADCAST_AUDIO_BECOMING_NOISY = 15; private static final int MSG_CONFIGURE_SAFE_MEDIA_VOLUME = 16; @@ -514,10 +515,6 @@ public class AudioService extends IAudioService.Stub { // Request to override default use of A2DP for media. private boolean mBluetoothA2dpEnabled; - // FIXME: remove when MediaRouter does not use setBluetoothA2dpOn() anymore - // state of bluetooth A2DP enable request sen by deprecated APIs setBluetoothA2dpOn() and - // isBluettohA2dpOn() - private boolean mBluetoothA2dpEnabledExternal; private final Object mBluetoothA2dpEnabledLock = new Object(); // Monitoring of audio routes. Protected by mCurAudioRoutes. @@ -2718,23 +2715,22 @@ public class AudioService extends IAudioService.Stub { return (mForcedUseForComm == AudioSystem.FORCE_BT_SCO); } - /** - * Deprecated. - * Keep stub implementation until MediaRouter stops using it. - * @deprecated - * */ + /** @see AudioManager#setBluetoothA2dpOn(boolean) */ public void setBluetoothA2dpOn(boolean on) { - mBluetoothA2dpEnabledExternal = on; - Log.e(TAG, "setBluetoothA2dpOn() is deprecated, now a no-op", - new Exception("Deprecated use of setBluetoothA2dpOn()")); + synchronized (mBluetoothA2dpEnabledLock) { + mBluetoothA2dpEnabled = on; + sendMsg(mAudioHandler, MSG_SET_FORCE_BT_A2DP_USE, SENDMSG_QUEUE, + AudioSystem.FOR_MEDIA, + mBluetoothA2dpEnabled ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP, + null, 0); + } } - /** Deprecated. - * Keep stub implementation until MediaRouter stops using it - * @deprecated - * */ + /** @see AudioManager#isBluetoothA2dpOn() */ public boolean isBluetoothA2dpOn() { - return mBluetoothA2dpEnabledExternal; + synchronized (mBluetoothA2dpEnabledLock) { + return mBluetoothA2dpEnabled; + } } /** @see AudioManager#startBluetoothSco() */ @@ -3807,11 +3803,6 @@ public class AudioService extends IAudioService.Stub { Slog.i(TAG, "setWiredDeviceConnectionState(" + state + " nm: " + name + " addr:" + address + ")"); } - if ((state == 0) && ((type == AudioSystem.DEVICE_OUT_WIRED_HEADSET) || - (type == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE) || - (type == AudioSystem.DEVICE_OUT_LINE))) { - setBluetoothA2dpOnInt(true); - } int delay = checkSendBecomingNoisyIntent(type, state); queueMsgUnderWakeLock(mAudioHandler, MSG_SET_WIRED_DEVICE_CONNECTION_STATE, @@ -4623,6 +4614,7 @@ public class AudioService extends IAudioService.Stub { break; case MSG_SET_FORCE_USE: + case MSG_SET_FORCE_BT_A2DP_USE: setForceUse(msg.arg1, msg.arg2); break; @@ -5030,7 +5022,6 @@ public class AudioService extends IAudioService.Stub { devices |= dev; } } - if (devices == device) { sendMsg(mAudioHandler, MSG_BROADCAST_AUDIO_BECOMING_NOISY, @@ -5126,6 +5117,11 @@ public class AudioService extends IAudioService.Stub { } synchronized (mConnectedDevices) { + if ((state == 0) && ((device == AudioSystem.DEVICE_OUT_WIRED_HEADSET) || + (device == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE) || + (device == AudioSystem.DEVICE_OUT_LINE))) { + setBluetoothA2dpOnInt(true); + } boolean isUsb = ((device & ~AudioSystem.DEVICE_OUT_ALL_USB) == 0) || (((device & AudioSystem.DEVICE_BIT_IN) != 0) && ((device & ~AudioSystem.DEVICE_IN_ALL_USB) == 0)); @@ -5609,6 +5605,7 @@ public class AudioService extends IAudioService.Stub { public void setBluetoothA2dpOnInt(boolean on) { synchronized (mBluetoothA2dpEnabledLock) { mBluetoothA2dpEnabled = on; + mAudioHandler.removeMessages(MSG_SET_FORCE_BT_A2DP_USE); setForceUseInt_SyncDevices(AudioSystem.FOR_MEDIA, mBluetoothA2dpEnabled ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP); } @@ -5623,6 +5620,8 @@ public class AudioService extends IAudioService.Stub { } else { // config == AudioSystem.FORCE_NONE mBecomingNoisyIntentDevices |= AudioSystem.DEVICE_OUT_ALL_A2DP; } + sendMsg(mAudioHandler, MSG_REPORT_NEW_ROUTES, + SENDMSG_NOOP, 0, 0, null, 0); break; case AudioSystem.FOR_DOCK: if (config == AudioSystem.FORCE_ANALOG_DOCK) { diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java index 66aa40325a4f..5772a57d79aa 100644 --- a/services/core/java/com/android/server/clipboard/ClipboardService.java +++ b/services/core/java/com/android/server/clipboard/ClipboardService.java @@ -20,7 +20,6 @@ import android.app.ActivityManagerNative; import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.IActivityManager; -import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipDescription; import android.content.ContentProvider; @@ -28,7 +27,6 @@ import android.content.IClipboard; import android.content.IOnPrimaryClipChangedListener; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -47,23 +45,57 @@ import android.os.UserManager; import android.util.Slog; import android.util.SparseArray; +import com.android.server.SystemService; + import java.util.HashSet; import java.util.List; /** * Implementation of the clipboard for copy and paste. */ -public class ClipboardService extends IClipboard.Stub { +public class ClipboardService extends SystemService { private static final String TAG = "ClipboardService"; - private final Context mContext; private final IActivityManager mAm; private final IUserManager mUm; private final PackageManager mPm; private final AppOpsManager mAppOps; private final IBinder mPermissionOwner; + private final SparseArray<PerUserClipboard> mClipboards = new SparseArray<>(); + + /** + * Instantiates the clipboard. + */ + public ClipboardService(Context context) { + super(context); + + mAm = ActivityManagerNative.getDefault(); + mPm = getContext().getPackageManager(); + mUm = (IUserManager) ServiceManager.getService(Context.USER_SERVICE); + mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE); + IBinder permOwner = null; + try { + permOwner = mAm.newUriPermissionOwner("clipboard"); + } catch (RemoteException e) { + Slog.w("clipboard", "AM dead", e); + } + mPermissionOwner = permOwner; + } + + @Override + public void onStart() { + publishBinderService(Context.CLIPBOARD_SERVICE, new ClipboardImpl()); + } + + @Override + public void onCleanupUser(int userId) { + synchronized (mClipboards) { + mClipboards.remove(userId); + } + } + private class ListenerInfo { final int mUid; final String mPackageName; @@ -89,52 +121,141 @@ public class ClipboardService extends IClipboard.Stub { } } - private SparseArray<PerUserClipboard> mClipboards = new SparseArray<PerUserClipboard>(); + private class ClipboardImpl extends IClipboard.Stub { + @Override + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + try { + return super.onTransact(code, data, reply, flags); + } catch (RuntimeException e) { + if (!(e instanceof SecurityException)) { + Slog.wtf("clipboard", "Exception: ", e); + } + throw e; + } - /** - * Instantiates the clipboard. - */ - public ClipboardService(Context context) { - mContext = context; - mAm = ActivityManagerNative.getDefault(); - mPm = context.getPackageManager(); - mUm = (IUserManager) ServiceManager.getService(Context.USER_SERVICE); - mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE); - IBinder permOwner = null; - try { - permOwner = mAm.newUriPermissionOwner("clipboard"); - } catch (RemoteException e) { - Slog.w("clipboard", "AM dead", e); } - mPermissionOwner = permOwner; - // Remove the clipboard if a user is removed - IntentFilter userFilter = new IntentFilter(); - userFilter.addAction(Intent.ACTION_USER_REMOVED); - mContext.registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (Intent.ACTION_USER_REMOVED.equals(action)) { - removeClipboard(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); + @Override + public void setPrimaryClip(ClipData clip, String callingPackage) { + synchronized (this) { + if (clip != null && clip.getItemCount() <= 0) { + throw new IllegalArgumentException("No items"); + } + final int callingUid = Binder.getCallingUid(); + if (mAppOps.noteOp(AppOpsManager.OP_WRITE_CLIPBOARD, callingUid, + callingPackage) != AppOpsManager.MODE_ALLOWED) { + return; + } + checkDataOwnerLocked(clip, callingUid); + final int userId = UserHandle.getUserId(callingUid); + PerUserClipboard clipboard = getClipboard(userId); + revokeUris(clipboard); + setPrimaryClipInternal(clipboard, clip); + List<UserInfo> related = getRelatedProfiles(userId); + if (related != null) { + int size = related.size(); + if (size > 1) { // Related profiles list include the current profile. + boolean canCopy = false; + try { + canCopy = !mUm.getUserRestrictions(userId).getBoolean( + UserManager.DISALLOW_CROSS_PROFILE_COPY_PASTE); + } catch (RemoteException e) { + Slog.e(TAG, "Remote Exception calling UserManager: " + e); + } + // Copy clip data to related users if allowed. If disallowed, then remove + // primary clip in related users to prevent pasting stale content. + if (!canCopy) { + clip = null; + } else { + // We want to fix the uris of the related user's clip without changing the + // uris of the current user's clip. + // So, copy the ClipData, and then copy all the items, so that nothing + // is shared in memmory. + clip = new ClipData(clip); + for (int i = clip.getItemCount() - 1; i >= 0; i--) { + clip.setItemAt(i, new ClipData.Item(clip.getItemAt(i))); + } + clip.fixUrisLight(userId); + } + for (int i = 0; i < size; i++) { + int id = related.get(i).id; + if (id != userId) { + setPrimaryClipInternal(getClipboard(id), clip); + } + } + } + } + } + } + + @Override + public ClipData getPrimaryClip(String pkg) { + synchronized (this) { + if (mAppOps.noteOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), + pkg) != AppOpsManager.MODE_ALLOWED) { + return null; } + addActiveOwnerLocked(Binder.getCallingUid(), pkg); + return getClipboard().primaryClip; } - }, userFilter); - } + } - @Override - public boolean onTransact(int code, Parcel data, Parcel reply, int flags) - throws RemoteException { - try { - return super.onTransact(code, data, reply, flags); - } catch (RuntimeException e) { - if (!(e instanceof SecurityException)) { - Slog.wtf("clipboard", "Exception: ", e); + @Override + public ClipDescription getPrimaryClipDescription(String callingPackage) { + synchronized (this) { + if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), + callingPackage) != AppOpsManager.MODE_ALLOWED) { + return null; + } + PerUserClipboard clipboard = getClipboard(); + return clipboard.primaryClip != null ? clipboard.primaryClip.getDescription() : null; } - throw e; } - - } + + @Override + public boolean hasPrimaryClip(String callingPackage) { + synchronized (this) { + if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), + callingPackage) != AppOpsManager.MODE_ALLOWED) { + return false; + } + return getClipboard().primaryClip != null; + } + } + + @Override + public void addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener, + String callingPackage) { + synchronized (this) { + getClipboard().primaryClipListeners.register(listener, + new ListenerInfo(Binder.getCallingUid(), callingPackage)); + } + } + + @Override + public void removePrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { + synchronized (this) { + getClipboard().primaryClipListeners.unregister(listener); + } + } + + @Override + public boolean hasClipboardText(String callingPackage) { + synchronized (this) { + if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), + callingPackage) != AppOpsManager.MODE_ALLOWED) { + return false; + } + PerUserClipboard clipboard = getClipboard(); + if (clipboard.primaryClip != null) { + CharSequence text = clipboard.primaryClip.getItemAt(0).getText(); + return text != null && text.length() > 0; + } + return false; + } + } + }; private PerUserClipboard getClipboard() { return getClipboard(UserHandle.getCallingUserId()); @@ -151,64 +272,6 @@ public class ClipboardService extends IClipboard.Stub { } } - private void removeClipboard(int userId) { - synchronized (mClipboards) { - mClipboards.remove(userId); - } - } - - public void setPrimaryClip(ClipData clip, String callingPackage) { - synchronized (this) { - if (clip != null && clip.getItemCount() <= 0) { - throw new IllegalArgumentException("No items"); - } - final int callingUid = Binder.getCallingUid(); - if (mAppOps.noteOp(AppOpsManager.OP_WRITE_CLIPBOARD, callingUid, - callingPackage) != AppOpsManager.MODE_ALLOWED) { - return; - } - checkDataOwnerLocked(clip, callingUid); - final int userId = UserHandle.getUserId(callingUid); - PerUserClipboard clipboard = getClipboard(userId); - revokeUris(clipboard); - setPrimaryClipInternal(clipboard, clip); - List<UserInfo> related = getRelatedProfiles(userId); - if (related != null) { - int size = related.size(); - if (size > 1) { // Related profiles list include the current profile. - boolean canCopy = false; - try { - canCopy = !mUm.getUserRestrictions(userId).getBoolean( - UserManager.DISALLOW_CROSS_PROFILE_COPY_PASTE); - } catch (RemoteException e) { - Slog.e(TAG, "Remote Exception calling UserManager: " + e); - } - // Copy clip data to related users if allowed. If disallowed, then remove - // primary clip in related users to prevent pasting stale content. - if (!canCopy) { - clip = null; - } else { - // We want to fix the uris of the related user's clip without changing the - // uris of the current user's clip. - // So, copy the ClipData, and then copy all the items, so that nothing - // is shared in memmory. - clip = new ClipData(clip); - for (int i = clip.getItemCount() - 1; i >= 0; i--) { - clip.setItemAt(i, new ClipData.Item(clip.getItemAt(i))); - } - clip.fixUrisLight(userId); - } - for (int i = 0; i < size; i++) { - int id = related.get(i).id; - if (id != userId) { - setPrimaryClipInternal(getClipboard(id), clip); - } - } - } - } - } - } - List<UserInfo> getRelatedProfiles(int userId) { final List<UserInfo> related; final long origId = Binder.clearCallingIdentity(); @@ -251,67 +314,6 @@ public class ClipboardService extends IClipboard.Stub { Binder.restoreCallingIdentity(ident); } } - - public ClipData getPrimaryClip(String pkg) { - synchronized (this) { - if (mAppOps.noteOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), - pkg) != AppOpsManager.MODE_ALLOWED) { - return null; - } - addActiveOwnerLocked(Binder.getCallingUid(), pkg); - return getClipboard().primaryClip; - } - } - - public ClipDescription getPrimaryClipDescription(String callingPackage) { - synchronized (this) { - if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), - callingPackage) != AppOpsManager.MODE_ALLOWED) { - return null; - } - PerUserClipboard clipboard = getClipboard(); - return clipboard.primaryClip != null ? clipboard.primaryClip.getDescription() : null; - } - } - - public boolean hasPrimaryClip(String callingPackage) { - synchronized (this) { - if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), - callingPackage) != AppOpsManager.MODE_ALLOWED) { - return false; - } - return getClipboard().primaryClip != null; - } - } - - public void addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener, - String callingPackage) { - synchronized (this) { - getClipboard().primaryClipListeners.register(listener, - new ListenerInfo(Binder.getCallingUid(), callingPackage)); - } - } - - public void removePrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { - synchronized (this) { - getClipboard().primaryClipListeners.unregister(listener); - } - } - - public boolean hasClipboardText(String callingPackage) { - synchronized (this) { - if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), - callingPackage) != AppOpsManager.MODE_ALLOWED) { - return false; - } - PerUserClipboard clipboard = getClipboard(); - if (clipboard.primaryClip != null) { - CharSequence text = clipboard.primaryClip.getItemAt(0).getText(); - return text != null && text.length() > 0; - } - return false; - } - } private final void checkUriOwnerLocked(Uri uri, int uid) { if (!"content".equals(uri.getScheme())) { diff --git a/services/core/java/com/android/server/connectivity/NetworkMonitor.java b/services/core/java/com/android/server/connectivity/NetworkMonitor.java index 42d80fc6fc48..6eb89facca76 100644 --- a/services/core/java/com/android/server/connectivity/NetworkMonitor.java +++ b/services/core/java/com/android/server/connectivity/NetworkMonitor.java @@ -23,7 +23,6 @@ import static android.net.CaptivePortal.APP_RETURN_WANTED_AS_IS; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -37,14 +36,12 @@ import android.net.Uri; import android.net.metrics.IpConnectivityLog; import android.net.metrics.NetworkEvent; import android.net.metrics.ValidationProbeEvent; +import android.net.util.Stopwatch; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; -import android.net.util.Stopwatch; import android.os.Handler; import android.os.Message; -import android.os.Process; import android.os.SystemClock; -import android.os.SystemProperties; import android.os.UserHandle; import android.provider.Settings; import android.telephony.CellIdentityCdma; @@ -66,27 +63,39 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Protocol; import com.android.internal.util.State; import com.android.internal.util.StateMachine; -import com.android.internal.util.WakeupMessage; import java.io.IOException; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.MalformedURLException; -import java.net.UnknownHostException; import java.net.URL; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicReference; +import java.net.UnknownHostException; import java.util.List; import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; /** * {@hide} */ public class NetworkMonitor extends StateMachine { - private static final boolean DBG = false; private static final String TAG = NetworkMonitor.class.getSimpleName(); - private static final String DEFAULT_SERVER = "connectivitycheck.gstatic.com"; + private static final boolean DBG = false; + + // Default configuration values for captive portal detection probes. + // TODO: append a random length parameter to the default HTTPS url. + // TODO: randomize browser version ids in the default User-Agent String. + private static final String DEFAULT_HTTPS_URL = "https://www.google.com/generate_204"; + private static final String DEFAULT_HTTP_URL = + "http://connectivitycheck.gstatic.com/generate_204"; + private static final String DEFAULT_FALLBACK_URL = "http://www.google.com/gen_204"; + private static final String DEFAULT_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) " + + "AppleWebKit/537.36 (KHTML, like Gecko) " + + "Chrome/52.0.2743.82 Safari/537.36"; + private static final int SOCKET_TIMEOUT_MS = 10000; + private static final int PROBE_TIMEOUT_MS = 3000; + public static final String ACTION_NETWORK_CONDITIONS_MEASURED = "android.net.conn.NETWORK_CONDITIONS_MEASURED"; public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type"; @@ -224,6 +233,9 @@ public class NetworkMonitor extends StateMachine { private final Stopwatch mEvaluationTimer = new Stopwatch(); + // This variable is set before transitioning to the mCaptivePortalState. + private CaptivePortalProbeResult mLastPortalProbeResult = CaptivePortalProbeResult.FAILED; + public NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo, NetworkRequest defaultRequest) { this(context, handler, networkAgentInfo, defaultRequest, new IpConnectivityLog()); @@ -389,6 +401,8 @@ public class NetworkMonitor extends StateMachine { sendMessage(CMD_CAPTIVE_PORTAL_APP_FINISHED, response); } })); + intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL, + mLastPortalProbeResult.detectUrl); intent.setFlags( Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivityAsUser(intent, UserHandle.CURRENT); @@ -412,14 +426,22 @@ public class NetworkMonitor extends StateMachine { */ @VisibleForTesting public static final class CaptivePortalProbeResult { - static final CaptivePortalProbeResult FAILED = new CaptivePortalProbeResult(599, null); + static final CaptivePortalProbeResult FAILED = new CaptivePortalProbeResult(599); - final int mHttpResponseCode; // HTTP response code returned from Internet probe. - final String mRedirectUrl; // Redirect destination returned from Internet probe. + private final int mHttpResponseCode; // HTTP response code returned from Internet probe. + final String redirectUrl; // Redirect destination returned from Internet probe. + final String detectUrl; // URL where a 204 response code indicates + // captive portal has been appeased. - public CaptivePortalProbeResult(int httpResponseCode, String redirectUrl) { + public CaptivePortalProbeResult( + int httpResponseCode, String redirectUrl, String detectUrl) { mHttpResponseCode = httpResponseCode; - mRedirectUrl = redirectUrl; + this.redirectUrl = redirectUrl; + this.detectUrl = detectUrl; + } + + public CaptivePortalProbeResult(int httpResponseCode) { + this(httpResponseCode, null, null); } boolean isSuccessful() { return mHttpResponseCode == 204; } @@ -492,7 +514,8 @@ public class NetworkMonitor extends StateMachine { transitionTo(mValidatedState); } else if (probeResult.isPortal()) { mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED, - NETWORK_TEST_RESULT_INVALID, mNetId, probeResult.mRedirectUrl)); + NETWORK_TEST_RESULT_INVALID, mNetId, probeResult.redirectUrl)); + mLastPortalProbeResult = probeResult; transitionTo(mCaptivePortalState); } else { final Message msg = obtainMessage(CMD_REEVALUATE, ++mReevaluateToken, 0); @@ -500,7 +523,7 @@ public class NetworkMonitor extends StateMachine { logNetworkEvent(NetworkEvent.NETWORK_VALIDATION_FAILED); mConnectivityServiceHandler.sendMessage(obtainMessage( EVENT_NETWORK_TESTED, NETWORK_TEST_RESULT_INVALID, mNetId, - probeResult.mRedirectUrl)); + probeResult.redirectUrl)); if (mAttempts >= BLAME_FOR_EVALUATION_ATTEMPTS) { // Don't continue to blame UID forever. TrafficStats.clearThreadStatsUid(); @@ -585,22 +608,33 @@ public class NetworkMonitor extends StateMachine { } } - private static String getCaptivePortalServerUrl(Context context, boolean isHttps) { - String server = Settings.Global.getString(context.getContentResolver(), - Settings.Global.CAPTIVE_PORTAL_SERVER); - if (server == null) server = DEFAULT_SERVER; - return (isHttps ? "https" : "http") + "://" + server + "/generate_204"; + private static String getCaptivePortalServerHttpsUrl(Context context) { + return getSetting(context, Settings.Global.CAPTIVE_PORTAL_HTTPS_URL, DEFAULT_HTTPS_URL); + } + + public static String getCaptivePortalServerHttpUrl(Context context) { + return getSetting(context, Settings.Global.CAPTIVE_PORTAL_HTTP_URL, DEFAULT_HTTP_URL); } - public static String getCaptivePortalServerUrl(Context context) { - return getCaptivePortalServerUrl(context, false); + private static String getCaptivePortalFallbackUrl(Context context) { + return getSetting(context, + Settings.Global.CAPTIVE_PORTAL_FALLBACK_URL, DEFAULT_FALLBACK_URL); + } + + private static String getCaptivePortalUserAgent(Context context) { + return getSetting(context, Settings.Global.CAPTIVE_PORTAL_USER_AGENT, DEFAULT_USER_AGENT); + } + + private static String getSetting(Context context, String symbol, String defaultValue) { + final String value = Settings.Global.getString(context.getContentResolver(), symbol); + return value != null ? value : defaultValue; } @VisibleForTesting protected CaptivePortalProbeResult isCaptivePortal() { - if (!mIsCaptivePortalCheckEnabled) return new CaptivePortalProbeResult(204, null); + if (!mIsCaptivePortalCheckEnabled) return new CaptivePortalProbeResult(204); - URL pacUrl = null, httpUrl = null, httpsUrl = null; + URL pacUrl = null, httpsUrl = null, httpUrl = null, fallbackUrl = null; // On networks with a PAC instead of fetching a URL that should result in a 204 // response, we instead simply fetch the PAC script. This is done for a few reasons: @@ -621,20 +655,17 @@ public class NetworkMonitor extends StateMachine { // results for network validation. final ProxyInfo proxyInfo = mNetworkAgentInfo.linkProperties.getHttpProxy(); if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) { - try { - pacUrl = new URL(proxyInfo.getPacFileUrl().toString()); - } catch (MalformedURLException e) { - validationLog("Invalid PAC URL: " + proxyInfo.getPacFileUrl().toString()); + pacUrl = makeURL(proxyInfo.getPacFileUrl().toString()); + if (pacUrl == null) { return CaptivePortalProbeResult.FAILED; } } if (pacUrl == null) { - try { - httpUrl = new URL(getCaptivePortalServerUrl(mContext, false)); - httpsUrl = new URL(getCaptivePortalServerUrl(mContext, true)); - } catch (MalformedURLException e) { - validationLog("Bad validation URL: " + getCaptivePortalServerUrl(mContext, false)); + httpsUrl = makeURL(getCaptivePortalServerHttpsUrl(mContext)); + httpUrl = makeURL(getCaptivePortalServerHttpUrl(mContext)); + fallbackUrl = makeURL(getCaptivePortalFallbackUrl(mContext)); + if (httpUrl == null || httpsUrl == null) { return CaptivePortalProbeResult.FAILED; } } @@ -680,7 +711,7 @@ public class NetworkMonitor extends StateMachine { if (pacUrl != null) { result = sendHttpProbe(pacUrl, ValidationProbeEvent.PROBE_PAC); } else if (mUseHttps) { - result = sendParallelHttpProbes(httpsUrl, httpUrl); + result = sendParallelHttpProbes(httpsUrl, httpUrl, fallbackUrl); } else { result = sendHttpProbe(httpUrl, ValidationProbeEvent.PROBE_HTTP); } @@ -710,6 +741,10 @@ public class NetworkMonitor extends StateMachine { urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS); urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS); urlConnection.setUseCaches(false); + final String userAgent = getCaptivePortalUserAgent(mContext); + if (userAgent != null) { + urlConnection.setRequestProperty("User-Agent", userAgent); + } // Time how long it takes to get a response to our request long requestTimestamp = SystemClock.elapsedRealtime(); @@ -755,28 +790,24 @@ public class NetworkMonitor extends StateMachine { } } logValidationProbe(probeTimer.stop(), probeType, httpResponseCode); - return new CaptivePortalProbeResult(httpResponseCode, redirectUrl); + return new CaptivePortalProbeResult(httpResponseCode, redirectUrl, url.toString()); } - private CaptivePortalProbeResult sendParallelHttpProbes(URL httpsUrl, URL httpUrl) { - // Number of probes to wait for. We might wait for all of them, but we might also return if - // only one of them has replied. For example, we immediately return if the HTTP probe finds - // a captive portal, even if the HTTPS probe is timing out. + private CaptivePortalProbeResult sendParallelHttpProbes( + URL httpsUrl, URL httpUrl, URL fallbackUrl) { + // Number of probes to wait for. If a probe completes with a conclusive answer + // it shortcuts the latch immediately by forcing the count to 0. final CountDownLatch latch = new CountDownLatch(2); - // Which probe result we're going to use. This doesn't need to be atomic, but it does need - // to be final because otherwise we can't set it from the ProbeThreads. - final AtomicReference<CaptivePortalProbeResult> finalResult = new AtomicReference<>(); - final class ProbeThread extends Thread { private final boolean mIsHttps; - private volatile CaptivePortalProbeResult mResult; + private volatile CaptivePortalProbeResult mResult = CaptivePortalProbeResult.FAILED; public ProbeThread(boolean isHttps) { mIsHttps = isHttps; } - public CaptivePortalProbeResult getResult() { + public CaptivePortalProbeResult result() { return mResult; } @@ -788,32 +819,66 @@ public class NetworkMonitor extends StateMachine { mResult = sendHttpProbe(httpUrl, ValidationProbeEvent.PROBE_HTTP); } if ((mIsHttps && mResult.isSuccessful()) || (!mIsHttps && mResult.isPortal())) { - // HTTPS succeeded, or HTTP found a portal. Don't wait for the other probe. - finalResult.compareAndSet(null, mResult); - latch.countDown(); + // Stop waiting immediately if https succeeds or if http finds a portal. + while (latch.getCount() > 0) { + latch.countDown(); + } } - // Signal that one probe has completed. If we've already made a decision, or if this - // is the second probe, the latch will be at zero and we'll return a result. + // Signal this probe has completed. latch.countDown(); } } - ProbeThread httpsProbe = new ProbeThread(true); - ProbeThread httpProbe = new ProbeThread(false); - httpsProbe.start(); - httpProbe.start(); + final ProbeThread httpsProbe = new ProbeThread(true); + final ProbeThread httpProbe = new ProbeThread(false); try { - latch.await(); + httpsProbe.start(); + httpProbe.start(); + latch.await(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { - validationLog("Error: probe wait interrupted!"); + validationLog("Error: probes wait interrupted!"); return CaptivePortalProbeResult.FAILED; } - // If there was no deciding probe, that means that both probes completed. Return HTTPS. - finalResult.compareAndSet(null, httpsProbe.getResult()); + final CaptivePortalProbeResult httpsResult = httpsProbe.result(); + final CaptivePortalProbeResult httpResult = httpProbe.result(); + + // Look for a conclusive probe result first. + if (httpResult.isPortal()) { + return httpResult; + } + // httpsResult.isPortal() is not expected, but check it nonetheless. + if (httpsResult.isPortal() || httpsResult.isSuccessful()) { + return httpsResult; + } + // If a fallback url is specified, use a fallback probe to try again portal detection. + if (fallbackUrl != null) { + CaptivePortalProbeResult result = + sendHttpProbe(fallbackUrl, ValidationProbeEvent.PROBE_FALLBACK); + if (result.isPortal()) { + return result; + } + } + // Otherwise wait until https probe completes and use its result. + try { + httpsProbe.join(); + } catch (InterruptedException e) { + validationLog("Error: https probe wait interrupted!"); + return CaptivePortalProbeResult.FAILED; + } + return httpsProbe.result(); + } - return finalResult.get(); + private URL makeURL(String url) { + if (url != null) { + try { + return new URL(url); + } catch (MalformedURLException e) { + validationLog("Bad URL: " + url); + } + } + return null; } /** diff --git a/services/core/java/com/android/server/fingerprint/FingerprintService.java b/services/core/java/com/android/server/fingerprint/FingerprintService.java index 5addffbeb426..9523a1ce20c5 100644 --- a/services/core/java/com/android/server/fingerprint/FingerprintService.java +++ b/services/core/java/com/android/server/fingerprint/FingerprintService.java @@ -195,6 +195,7 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe public void binderDied() { Slog.v(TAG, "fingerprintd died"); mDaemon = null; + mCurrentUserId = UserHandle.USER_CURRENT; handleError(mHalDeviceId, FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE); } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 74095acca16a..87f4030a9049 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -18,8 +18,8 @@ package com.android.server.input; import android.annotation.NonNull; import android.annotation.Nullable; -import android.os.Build; import android.os.LocaleList; +import android.os.ShellCallback; import android.util.Log; import android.view.Display; import com.android.internal.inputmethod.InputMethodSubtypeHandle; @@ -91,10 +91,8 @@ import android.view.PointerIcon; import android.view.Surface; import android.view.ViewConfiguration; import android.view.WindowManagerPolicy; -import android.view.inputmethod.InputMethod; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; -import android.widget.Toast; import java.io.File; import java.io.FileDescriptor; @@ -103,11 +101,8 @@ import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -1739,8 +1734,9 @@ public class InputManagerService extends IInputManager.Stub @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, - FileDescriptor err, String[] args, ResultReceiver resultReceiver) { - (new Shell()).exec(this, in, out, err, args, resultReceiver); + FileDescriptor err, String[] args, ShellCallback callback, + ResultReceiver resultReceiver) { + (new Shell()).exec(this, in, out, err, args, callback, resultReceiver); } public int onShellCommand(Shell shell, String cmd) { diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java index 9d931467d914..fe3a02d193f6 100644 --- a/services/core/java/com/android/server/job/JobSchedulerService.java +++ b/services/core/java/com/android/server/job/JobSchedulerService.java @@ -44,9 +44,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; -import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.content.pm.PackageManager.NameNotFoundException; @@ -62,6 +60,7 @@ import android.os.PowerManager; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; +import android.os.ShellCallback; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; @@ -1718,9 +1717,9 @@ public final class JobSchedulerService extends com.android.server.SystemService @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, - String[] args, ResultReceiver resultReceiver) throws RemoteException { + String[] args, ShellCallback callback, ResultReceiver resultReceiver) { (new JobSchedulerShellCommand(JobSchedulerService.this)).exec( - this, in, out, err, args, resultReceiver); + this, in, out, err, args, callback, resultReceiver); } }; diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java index 6381aa747022..547cc51e9a05 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java +++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java @@ -123,7 +123,6 @@ import android.net.LinkProperties; import android.net.NetworkIdentity; import android.net.NetworkInfo; import android.net.NetworkPolicy; -import android.net.NetworkPolicyManager; import android.net.NetworkQuotaInfo; import android.net.NetworkState; import android.net.NetworkTemplate; @@ -144,6 +143,7 @@ import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; +import android.os.ShellCallback; import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; @@ -2386,9 +2386,9 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, - String[] args, ResultReceiver resultReceiver) throws RemoteException { + String[] args, ShellCallback callback, ResultReceiver resultReceiver) { (new NetworkPolicyManagerShellCommand(mContext, this)).exec( - this, in, out, err, args, resultReceiver); + this, in, out, err, args, callback, resultReceiver); } @Override diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerShellCommand.java b/services/core/java/com/android/server/net/NetworkPolicyManagerShellCommand.java index 4ca69dc8364f..8ced1c22020b 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyManagerShellCommand.java +++ b/services/core/java/com/android/server/net/NetworkPolicyManagerShellCommand.java @@ -333,9 +333,11 @@ class NetworkPolicyManagerShellCommand extends ShellCommand { System.arraycopy(policies, 0, newPolicies, 0, policies.length); newPolicies[newPolicies.length - 1] = policy; mInterface.setNetworkPolicies(newPolicies); + return 0; } } - return 0; + pw.print("Error: didn't find network with SSID "); pw.println(id); + return -1; } private List<NetworkPolicy> getWifiPolicies() throws RemoteException { diff --git a/services/core/java/com/android/server/pm/OtaDexoptService.java b/services/core/java/com/android/server/pm/OtaDexoptService.java index 689917cd670a..f777aaebdd14 100644 --- a/services/core/java/com/android/server/pm/OtaDexoptService.java +++ b/services/core/java/com/android/server/pm/OtaDexoptService.java @@ -28,6 +28,7 @@ import android.os.Environment; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; +import android.os.ShellCallback; import android.os.storage.StorageManager; import android.util.Log; import android.util.Slog; @@ -107,9 +108,9 @@ public class OtaDexoptService extends IOtaDexopt.Stub { @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, - String[] args, ResultReceiver resultReceiver) throws RemoteException { + String[] args, ShellCallback callback, ResultReceiver resultReceiver) { (new OtaDexoptShellCommand(this)).exec( - this, in, out, err, args, resultReceiver); + this, in, out, err, args, callback, resultReceiver); } @Override diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index c76302c1053c..df02b86956cc 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -185,6 +185,7 @@ import android.os.RemoteException; import android.os.ResultReceiver; import android.os.SELinux; import android.os.ServiceManager; +import android.os.ShellCallback; import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; @@ -464,6 +465,12 @@ public class PackageManagerService extends IPackageManager.Stub { private static final String PACKAGE_SCHEME = "package"; private static final String VENDOR_OVERLAY_DIR = "/vendor/overlay"; + /** + * If VENDOR_OVERLAY_SKU_PROPERTY is set, search for runtime resource overlay APKs in + * VENDOR_OVERLAY_DIR/<value of VENDOR_OVERLAY_SKU_PROPERTY> rather than in + * VENDOR_OVERLAY_DIR. + */ + private static final String VENDOR_OVERLAY_SKU_PROPERTY = "ro.boot.vendor.overlay.sku"; private static int DEFAULT_EPHEMERAL_HASH_PREFIX_MASK = 0xFFFFF000; private static int DEFAULT_EPHEMERAL_HASH_PREFIX_COUNT = 5; @@ -2281,8 +2288,14 @@ public class PackageManagerService extends IPackageManager.Stub { // Collect vendor overlay packages. // (Do this before scanning any apps.) // For security and version matching reason, only consider - // overlay packages if they reside in VENDOR_OVERLAY_DIR. - File vendorOverlayDir = new File(VENDOR_OVERLAY_DIR); + // overlay packages if they reside in the right directory. + File vendorOverlayDir; + String overlaySkuDir = SystemProperties.get(VENDOR_OVERLAY_SKU_PROPERTY); + if (!overlaySkuDir.isEmpty()) { + vendorOverlayDir = new File(VENDOR_OVERLAY_DIR, overlaySkuDir); + } else { + vendorOverlayDir = new File(VENDOR_OVERLAY_DIR); + } scanDirTracedLI(vendorOverlayDir, mDefParseFlags | PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR @@ -18397,9 +18410,10 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName()); @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, - FileDescriptor err, String[] args, ResultReceiver resultReceiver) { + FileDescriptor err, String[] args, ShellCallback callback, + ResultReceiver resultReceiver) { (new PackageManagerShellCommand(this)).exec( - this, in, out, err, args, resultReceiver); + this, in, out, err, args, callback, resultReceiver); } @Override diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 61374f3be093..1d3471a2d340 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -1160,14 +1160,15 @@ class PackageManagerShellCommand extends ShellCommand { private int doWriteSplit(int sessionId, String inPath, long sizeBytes, String splitName, boolean logSuccess) throws RemoteException { final PrintWriter pw = getOutPrintWriter(); - if ("-".equals(inPath)) { - inPath = null; - } else if (inPath != null) { - final File file = new File(inPath); - if (file.isFile()) { - sizeBytes = file.length(); - } + if (sizeBytes <= 0) { + pw.println("Error: must specify a APK size"); + return 1; + } + if (inPath != null && !"-".equals(inPath)) { + pw.println("Error: APK content must be streamed"); + return 1; } + inPath = null; final SessionInfo info = mInterface.getPackageInstaller().getSessionInfo(sessionId); diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 13f558e3dd13..19bf751417ce 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -67,6 +67,7 @@ import android.os.RemoteException; import android.os.ResultReceiver; import android.os.SELinux; import android.os.ServiceManager; +import android.os.ShellCallback; import android.os.ShellCommand; import android.os.SystemClock; import android.os.UserHandle; @@ -3366,13 +3367,14 @@ public class ShortcutService extends IShortcutService.Stub { @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, - String[] args, ResultReceiver resultReceiver) throws RemoteException { + String[] args, ShellCallback callback, ResultReceiver resultReceiver) { enforceShell(); final long token = injectClearCallingIdentity(); try { - final int status = (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver); + final int status = (new MyShellCommand()).exec(this, in, out, err, args, callback, + resultReceiver); resultReceiver.send(status, null); } finally { injectRestoreCallingIdentity(token); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index c1cb032ce97f..533d9b5b6c73 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -62,6 +62,7 @@ import android.os.RemoteException; import android.os.ResultReceiver; import android.os.SELinux; import android.os.ServiceManager; +import android.os.ShellCallback; import android.os.ShellCommand; import android.os.UserHandle; import android.os.UserManager; @@ -1705,14 +1706,13 @@ public class UserManagerService extends IUserManager.Stub { UserRestrictionsUtils .readRestrictions(parser, mGuestRestrictions); } - } else if (parser.getName().equals(TAG_DEVICE_POLICY_RESTRICTIONS) - ) { - UserRestrictionsUtils.readRestrictions(parser, - newDevicePolicyGlobalUserRestrictions); } break; } } + } else if (name.equals(TAG_DEVICE_POLICY_RESTRICTIONS)) { + UserRestrictionsUtils.readRestrictions(parser, + newDevicePolicyGlobalUserRestrictions); } else if (name.equals(TAG_GLOBAL_RESTRICTION_OWNER_ID)) { String ownerUserId = parser.getAttributeValue(null, ATTR_ID); if (ownerUserId != null) { @@ -3209,8 +3209,9 @@ public class UserManagerService extends IUserManager.Stub { @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, - FileDescriptor err, String[] args, ResultReceiver resultReceiver) { - (new Shell()).exec(this, in, out, err, args, resultReceiver); + FileDescriptor err, String[] args, ShellCallback callback, + ResultReceiver resultReceiver) { + (new Shell()).exec(this, in, out, err, args, callback, resultReceiver); } int onShellCommand(Shell shell, String cmd) { diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index b5559382ff2a..b3430c655023 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -182,6 +182,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { // No longer recommended for desk docks; static final boolean ENABLE_DESK_DOCK_HOME_CAPTURE = false; + static final boolean ALTERNATE_CAR_MODE_NAV_SIZE = false; + static final int SHORT_PRESS_POWER_NOTHING = 0; static final int SHORT_PRESS_POWER_GO_TO_SLEEP = 1; static final int SHORT_PRESS_POWER_REALLY_GO_TO_SLEEP = 2; @@ -1373,8 +1375,13 @@ public class PhoneWindowManager implements WindowManagerPolicy { case LONG_PRESS_BACK_NOTHING: break; case LONG_PRESS_BACK_GO_TO_VOICE_ASSIST: - Intent intent = new Intent(Intent.ACTION_VOICE_ASSIST); - startActivityAsUser(intent, UserHandle.CURRENT_OR_SELF); + final boolean keyguardActive = mKeyguardDelegate == null + ? false + : mKeyguardDelegate.isShowing(); + if (!keyguardActive) { + Intent intent = new Intent(Intent.ACTION_VOICE_ASSIST); + startActivityAsUser(intent, UserHandle.CURRENT_OR_SELF); + } break; } } @@ -2451,22 +2458,24 @@ public class PhoneWindowManager implements WindowManagerPolicy { mNavigationBarWidthForRotationDefault[mSeascapeRotation] = res.getDimensionPixelSize(com.android.internal.R.dimen.navigation_bar_width); - // Height of the navigation bar when presented horizontally at bottom - mNavigationBarHeightForRotationInCarMode[mPortraitRotation] = - mNavigationBarHeightForRotationInCarMode[mUpsideDownRotation] = - res.getDimensionPixelSize( - com.android.internal.R.dimen.navigation_bar_height_car_mode); - mNavigationBarHeightForRotationInCarMode[mLandscapeRotation] = - mNavigationBarHeightForRotationInCarMode[mSeascapeRotation] = res.getDimensionPixelSize( - com.android.internal.R.dimen.navigation_bar_height_landscape_car_mode); + if (ALTERNATE_CAR_MODE_NAV_SIZE) { + // Height of the navigation bar when presented horizontally at bottom + mNavigationBarHeightForRotationInCarMode[mPortraitRotation] = + mNavigationBarHeightForRotationInCarMode[mUpsideDownRotation] = + res.getDimensionPixelSize( + com.android.internal.R.dimen.navigation_bar_height_car_mode); + mNavigationBarHeightForRotationInCarMode[mLandscapeRotation] = + mNavigationBarHeightForRotationInCarMode[mSeascapeRotation] = res.getDimensionPixelSize( + com.android.internal.R.dimen.navigation_bar_height_landscape_car_mode); - // Width of the navigation bar when presented vertically along one side - mNavigationBarWidthForRotationInCarMode[mPortraitRotation] = - mNavigationBarWidthForRotationInCarMode[mUpsideDownRotation] = - mNavigationBarWidthForRotationInCarMode[mLandscapeRotation] = - mNavigationBarWidthForRotationInCarMode[mSeascapeRotation] = - res.getDimensionPixelSize( - com.android.internal.R.dimen.navigation_bar_width_car_mode); + // Width of the navigation bar when presented vertically along one side + mNavigationBarWidthForRotationInCarMode[mPortraitRotation] = + mNavigationBarWidthForRotationInCarMode[mUpsideDownRotation] = + mNavigationBarWidthForRotationInCarMode[mLandscapeRotation] = + mNavigationBarWidthForRotationInCarMode[mSeascapeRotation] = + res.getDimensionPixelSize( + com.android.internal.R.dimen.navigation_bar_width_car_mode); + } } /** {@inheritDoc} */ @@ -2598,7 +2607,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } private int getNavigationBarWidth(int rotation, int uiMode) { - if ((uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_CAR) { + if (ALTERNATE_CAR_MODE_NAV_SIZE && (uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_CAR) { return mNavigationBarWidthForRotationInCarMode[rotation]; } else { return mNavigationBarWidthForRotationDefault[rotation]; @@ -2619,7 +2628,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } private int getNavigationBarHeight(int rotation, int uiMode) { - if ((uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_CAR) { + if (ALTERNATE_CAR_MODE_NAV_SIZE && (uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_CAR) { return mNavigationBarHeightForRotationInCarMode[rotation]; } else { return mNavigationBarHeightForRotationDefault[rotation]; diff --git a/services/core/java/com/android/server/power/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java index 1dbc6d944b1c..badee82becbd 100644 --- a/services/core/java/com/android/server/power/ShutdownThread.java +++ b/services/core/java/com/android/server/power/ShutdownThread.java @@ -717,6 +717,14 @@ public final class ShutdownThread extends Thread { } if (!done[0]) { Log.w(TAG, "Timed out waiting for uncrypt."); + final int uncryptTimeoutError = 100; + String timeoutMessage = String.format("uncrypt_time: %d\n" + "uncrypt_error: %d\n", + MAX_UNCRYPT_WAIT_TIME / 1000, uncryptTimeoutError); + try { + FileUtils.stringToFile(RecoverySystem.UNCRYPT_STATUS_FILE, timeoutMessage); + } catch (IOException e) { + Log.e(TAG, "Failed to write timeout message to uncrypt status", e); + } } } } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 552803f47652..7b7db0e1440b 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -28,23 +28,21 @@ import android.os.IBinder; import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; +import android.os.ShellCallback; import android.os.UserHandle; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Slog; -import android.view.KeyEvent; import com.android.internal.statusbar.IStatusBar; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; import com.android.internal.statusbar.StatusBarIcon; -import com.android.internal.util.FastPrintWriter; import com.android.server.LocalServices; import com.android.server.notification.NotificationDelegate; import com.android.server.wm.WindowManagerService; import java.io.FileDescriptor; -import java.io.FileOutputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -849,9 +847,9 @@ public class StatusBarManagerService extends IStatusBarService.Stub { @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, - String[] args, ResultReceiver resultReceiver) throws RemoteException { + String[] args, ShellCallback callback, ResultReceiver resultReceiver) { (new StatusBarShellCommand(this)).exec( - this, in, out, err, args, resultReceiver); + this, in, out, err, args, callback, resultReceiver); } // ================================================================================ diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java index 846169cbf9c3..43cdf5978068 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java @@ -25,10 +25,10 @@ import android.os.Binder; import android.os.PatternMatcher; import android.os.Process; import android.os.ResultReceiver; +import android.os.ShellCallback; import android.os.UserHandle; import android.util.Slog; import android.webkit.IWebViewUpdateService; -import android.webkit.WebViewFactory; import android.webkit.WebViewProviderInfo; import android.webkit.WebViewProviderResponse; @@ -140,9 +140,10 @@ public class WebViewUpdateService extends SystemService { @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, - FileDescriptor err, String[] args, ResultReceiver resultReceiver) { + FileDescriptor err, String[] args, ShellCallback callback, + ResultReceiver resultReceiver) { (new WebViewUpdateServiceShellCommand(this)).exec( - this, in, out, err, args, resultReceiver); + this, in, out, err, args, callback, resultReceiver); } diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java index e4ec295fa34e..cd46165b56b3 100644 --- a/services/core/java/com/android/server/wm/AppTransition.java +++ b/services/core/java/com/android/server/wm/AppTransition.java @@ -374,6 +374,7 @@ public class AppTransition implements Dump { void goodToGo(AppWindowAnimator topOpeningAppAnimator, AppWindowAnimator topClosingAppAnimator, ArraySet<AppWindowToken> openingApps, ArraySet<AppWindowToken> closingApps) { + int appTransition = mNextAppTransition; mNextAppTransition = TRANSIT_UNSET; mAppTransitionState = APP_STATE_RUNNING; notifyAppTransitionStartingLocked( @@ -382,7 +383,7 @@ public class AppTransition implements Dump { topOpeningAppAnimator != null ? topOpeningAppAnimator.animation : null, topClosingAppAnimator != null ? topClosingAppAnimator.animation : null); mService.getDefaultDisplayContentLocked().getDockedDividerController() - .notifyAppTransitionStarting(openingApps); + .notifyAppTransitionStarting(openingApps, appTransition); // Prolong the start for the transition when docking a task from recents, unless recents // ended it already then we don't need to wait. diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java index ba26e13f69c3..6075c12b3434 100644 --- a/services/core/java/com/android/server/wm/AppWindowToken.java +++ b/services/core/java/com/android/server/wm/AppWindowToken.java @@ -429,7 +429,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree for (int i = 0; i < displayList.size(); i++) { final DisplayContent displayContent = displayList.get(i); mService.mLayersController.assignLayersLocked(displayContent.getWindowList()); - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); } } @@ -729,9 +729,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree if (mTask.mPreparedFrozenMergedConfig.equals(Configuration.EMPTY)) { // We didn't call prepareFreezingBounds on the task, so use the current value. - final Configuration config = new Configuration(mService.mGlobalConfiguration); - config.updateFrom(mTask.mOverrideConfig); - mFrozenMergedConfig.offer(config); + mFrozenMergedConfig.offer(new Configuration(mTask.getConfiguration())); } else { mFrozenMergedConfig.offer(new Configuration(mTask.mPreparedFrozenMergedConfig)); } @@ -792,6 +790,9 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree void overridePlayingAppAnimations(Animation a) { if (mAppAnimator.isAnimating()) { final WindowState win = findMainWindow(); + if (win == null) { + return; + } final int width = win.mContainingFrame.width(); final int height = win.mContainingFrame.height(); mAppAnimator.setAnimation(a, width, height, false, STACK_CLIP_NONE); @@ -957,7 +958,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree mService.updateFocusedWindowLocked( UPDATE_FOCUS_WILL_PLACE_SURFACES, true /*updateInputWindows*/); - mService.getDefaultDisplayContentLocked().layoutNeeded = true; + mService.getDefaultDisplayContentLocked().setLayoutNeeded(); mService.mWindowPlacerLocked.performSurfacePlacement(); Binder.restoreCallingIdentity(origId); return true; @@ -1065,7 +1066,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree allDrawn = true; // Force an additional layout pass where // WindowStateAnimator#commitFinishDrawingLocked() will call performShowLocked(). - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); mService.mH.obtainMessage(NOTIFY_ACTIVITY_DRAWN, token).sendToTarget(); } } @@ -1076,7 +1077,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree + " interesting=" + numInteresting + " drawn=" + numDrawnWindowsExcludingSaved); allDrawnExcludingSaved = true; - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); if (isAnimatingInvisibleWithSavedSurface() && !mService.mFinishedEarlyAnim.contains(this)) { mService.mFinishedEarlyAnim.add(this); diff --git a/services/core/java/com/android/server/wm/DimLayerController.java b/services/core/java/com/android/server/wm/DimLayerController.java index 7f97c463774e..da2c6a741100 100644 --- a/services/core/java/com/android/server/wm/DimLayerController.java +++ b/services/core/java/com/android/server/wm/DimLayerController.java @@ -256,7 +256,7 @@ class DimLayerController { // on whether a dim layer is showing or not. if (targetAlpha == 0) { mDisplayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_LAYOUT; - mDisplayContent.layoutNeeded = true; + mDisplayContent.setLayoutNeeded(); } } } else if (state.dimLayer.getLayer() != dimLayer) { diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index a09c5976e2ec..34a7390665ca 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -20,6 +20,7 @@ import static android.app.ActivityManager.StackId.DOCKED_STACK_ID; import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID; import static android.app.ActivityManager.StackId.HOME_STACK_ID; import static android.app.ActivityManager.StackId.PINNED_STACK_ID; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.view.WindowManager.DOCKED_BOTTOM; @@ -37,6 +38,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ADD_REMOVE; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DISPLAY; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_FOCUS; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_FOCUS_LIGHT; +import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WINDOW_MOVEMENT; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_VISIBILITY; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ORIENTATION; @@ -45,6 +47,7 @@ import static com.android.server.wm.WindowState.RESIZE_HANDLE_WIDTH_IN_DP; import android.annotation.NonNull; import android.app.ActivityManager.StackId; +import android.content.res.Configuration; import android.graphics.Rect; import android.graphics.Region; import android.graphics.Region.Op; @@ -99,7 +102,7 @@ class DisplayContent extends WindowContainer<TaskStack> { private Rect mContentRect = new Rect(); // Accessed directly by all users. - boolean layoutNeeded; + private boolean mLayoutNeeded; int pendingLayoutChanges; final boolean isDefaultDisplay; @@ -208,18 +211,26 @@ class DisplayContent extends WindowContainer<TaskStack> { return null; } - /** Callback used to notify about configuration changes. */ - void onConfigurationChanged(@NonNull List<Integer> changedStackList) { + @Override + void onConfigurationChanged(Configuration newParentConfig) { + super.onConfigurationChanged(newParentConfig); + // The display size information is heavily dependent on the resources in the current // configuration, so we need to reconfigure it every time the configuration changes. // See {@link PhoneWindowManager#setInitialDisplaySize}...sigh... mService.reconfigureDisplayLocked(this); getDockedDividerController().onConfigurationChanged(); + } - for (int i = 0; i < mChildren.size(); i++) { + /** + * Callback used to trigger bounds update after configuration change and get ids of stacks whose + * bounds were updated. + */ + void updateStackBoundsAfterConfigChange(@NonNull List<Integer> changedStackList) { + for (int i = mChildren.size() - 1; i >= 0; --i) { final TaskStack stack = mChildren.get(i); - if (stack.onConfigurationChanged()) { + if (stack.updateBoundsAfterConfigChange()) { changedStackList.add(stack.mStackId); } } @@ -271,7 +282,7 @@ class DisplayContent extends WindowContainer<TaskStack> { } final int orientation = super.getOrientation(); - if (orientation != SCREEN_ORIENTATION_UNSET) { + if (orientation != SCREEN_ORIENTATION_UNSET && orientation != SCREEN_ORIENTATION_BEHIND) { if (DEBUG_ORIENTATION) Slog.v(TAG_WM, "App is requesting an orientation, return " + orientation); return orientation; @@ -366,7 +377,7 @@ class DisplayContent extends WindowContainer<TaskStack> { } } addChild(stack, addIndex); - layoutNeeded = true; + setLayoutNeeded(); } /** @@ -683,8 +694,8 @@ class DisplayContent extends WindowContainer<TaskStack> { pw.print("x"); pw.print(mDisplayInfo.smallestNominalAppHeight); pw.print("-"); pw.print(mDisplayInfo.largestNominalAppWidth); pw.print("x"); pw.println(mDisplayInfo.largestNominalAppHeight); - pw.print(subPrefix); pw.print("deferred="); pw.print(mDeferredRemoval); - pw.print(" layoutNeeded="); pw.println(layoutNeeded); + pw.println(subPrefix + "deferred=" + mDeferredRemoval + + " mLayoutNeeded=" + mLayoutNeeded); pw.println(); pw.println(" Application tokens in top down Z order:"); @@ -777,7 +788,8 @@ class DisplayContent extends WindowContainer<TaskStack> { for (int i = 0; i < windowCount; i++) { WindowState window = windows.get(i); if (window.mAttrs.type == TYPE_TOAST && window.mOwnerUid == uid - && !window.mPermanentlyHidden && !window.mAnimatingExit) { + && !window.mPermanentlyHidden && !window.mAnimatingExit + && !window.mRemoveOnExit) { return false; } } @@ -1089,7 +1101,7 @@ class DisplayContent extends WindowContainer<TaskStack> { i -= lastBelow; if (i != numRemoved) { - layoutNeeded = true; + setLayoutNeeded(); Slog.w(TAG_WM, "On display=" + mDisplayId + " Rebuild removed " + numRemoved + " windows but added " + i + " rebuildAppWindowListLocked() " + " callers=" + Debug.getCallers(10)); @@ -1126,6 +1138,20 @@ class DisplayContent extends WindowContainer<TaskStack> { return windowList; } + void setLayoutNeeded() { + if (DEBUG_LAYOUT) Slog.w(TAG_WM, "setLayoutNeeded: callers=" + Debug.getCallers(3)); + mLayoutNeeded = true; + } + + void clearLayoutNeeded() { + if (DEBUG_LAYOUT) Slog.w(TAG_WM, "clearLayoutNeeded: callers=" + Debug.getCallers(3)); + mLayoutNeeded = false; + } + + boolean isLayoutNeeded() { + return mLayoutNeeded; + } + private int addAppWindowExisting(WindowState win, WindowList tokenWindowList) { int tokenWindowsPos; diff --git a/services/core/java/com/android/server/wm/DockedStackDividerController.java b/services/core/java/com/android/server/wm/DockedStackDividerController.java index ef8f492f5a6f..58541973a650 100644 --- a/services/core/java/com/android/server/wm/DockedStackDividerController.java +++ b/services/core/java/com/android/server/wm/DockedStackDividerController.java @@ -29,6 +29,7 @@ import static android.view.WindowManager.DOCKED_RIGHT; import static android.view.WindowManager.DOCKED_TOP; import static com.android.server.wm.AppTransition.DEFAULT_APP_TRANSITION_DURATION; import static com.android.server.wm.AppTransition.TOUCH_RESPONSE_INTERPOLATOR; +import static com.android.server.wm.AppTransition.TRANSIT_NONE; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import static com.android.server.wm.WindowManagerService.H.NOTIFY_DOCKED_STACK_MINIMIZED_CHANGED; @@ -153,7 +154,7 @@ public class DockedStackDividerController implements DimLayerUser { // If the bounds are fullscreen, return the value of the fullscreen configuration if (bounds == null || (bounds.left == 0 && bounds.top == 0 && bounds.right == di.logicalWidth && bounds.bottom == di.logicalHeight)) { - return mService.mGlobalConfiguration.smallestScreenWidthDp; + return mDisplayContent.getConfiguration().smallestScreenWidthDp; } final int baseDisplayWidth = mDisplayContent.mBaseDisplayWidth; final int baseDisplayHeight = mDisplayContent.mBaseDisplayHeight; @@ -190,7 +191,7 @@ public class DockedStackDividerController implements DimLayerUser { } private void initSnapAlgorithmForRotations() { - final Configuration baseConfig = mService.mGlobalConfiguration; + final Configuration baseConfig = mDisplayContent.getConfiguration(); // Initialize the snap algorithms for all 4 screen orientations. final Configuration config = new Configuration(); @@ -493,7 +494,7 @@ public class DockedStackDividerController implements DimLayerUser { checkMinimizeChanged(false /* animate */); } - void notifyAppTransitionStarting(ArraySet<AppWindowToken> openingApps) { + void notifyAppTransitionStarting(ArraySet<AppWindowToken> openingApps, int appTransition) { final boolean wasMinimized = mMinimizedDock; checkMinimizeChanged(true /* animate */); @@ -502,7 +503,8 @@ public class DockedStackDividerController implements DimLayerUser { // any case that was missed in ActivityStarter.postStartActivityUncheckedProcessing because // we couldn't retrace the launch of the app in the docked stack to the launch from // homescreen. - if (wasMinimized && mMinimizedDock && containsAppInDockedStack(openingApps)) { + if (wasMinimized && mMinimizedDock && containsAppInDockedStack(openingApps) + && appTransition != TRANSIT_NONE) { mService.showRecentApps(true /* fromHome */); } } diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 8f533fb1ac61..219fd8ed6e50 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -29,7 +29,6 @@ import android.os.UserHandle; import android.provider.Settings; import android.util.EventLog; import android.util.Slog; -import android.util.SparseArray; import android.util.SparseIntArray; import android.view.Display; import android.view.DisplayInfo; @@ -265,11 +264,11 @@ class RootWindowContainer extends WindowContainer<DisplayContent> { return bounds; } - boolean layoutNeeded() { + boolean isLayoutNeeded() { final int numDisplays = mChildren.size(); for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) { final DisplayContent displayContent = mChildren.get(displayNdx); - if (displayContent.layoutNeeded) { + if (displayContent.isLayoutNeeded()) { return true; } } @@ -347,18 +346,35 @@ class RootWindowContainer extends WindowContainer<DisplayContent> { } } - int[] onConfigurationChanged(Configuration config) { + /** Set new config and return array of ids of stacks that were changed during update. */ + int[] setGlobalConfigurationIfNeeded(Configuration newConfiguration) { + final boolean configChanged = getConfiguration().diff(newConfiguration) != 0; + if (!configChanged) { + return null; + } + onConfigurationChanged(newConfiguration); + return updateStackBoundsAfterConfigChange(); + } + + @Override + void onConfigurationChanged(Configuration newParentConfig) { prepareFreezingTaskBounds(); - mService.mGlobalConfiguration = new Configuration(config); + super.onConfigurationChanged(newParentConfig); mService.mPolicy.onConfigurationChanged(); + } + /** + * Callback used to trigger bounds update after configuration change and get ids of stacks whose + * bounds were updated. + */ + int[] updateStackBoundsAfterConfigChange() { mChangedStackList.clear(); final int numDisplays = mChildren.size(); for (int i = 0; i < numDisplays; ++i) { final DisplayContent dc = mChildren.get(i); - dc.onConfigurationChanged(mChangedStackList); + dc.updateStackBoundsAfterConfigChange(mChangedStackList); } return mChangedStackList.isEmpty() ? null : ArrayUtils.convertToIntArray(mChangedStackList); @@ -753,7 +769,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> { } } - if (layoutNeeded()) { + if (isLayoutNeeded()) { defaultDisplay.pendingLayoutChanges |= FINISH_LAYOUT_REDO_LAYOUT; if (DEBUG_LAYOUT_REPEATS) surfacePlacer.debugLayoutRepeats("mLayoutNeeded", defaultDisplay.pendingLayoutChanges); @@ -840,13 +856,13 @@ class RootWindowContainer extends WindowContainer<DisplayContent> { if (wallpaperDestroyed) { defaultDisplay.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER; - defaultDisplay.layoutNeeded = true; + defaultDisplay.setLayoutNeeded(); } for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) { final DisplayContent displayContent = mChildren.get(displayNdx); if (displayContent.pendingLayoutChanges != 0) { - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); } } @@ -901,8 +917,8 @@ class RootWindowContainer extends WindowContainer<DisplayContent> { } if (mService.mWaitingForDrawnCallback != null || - (mOrientationChangeComplete && !defaultDisplay.layoutNeeded && - !mUpdateRotation)) { + (mOrientationChangeComplete && !defaultDisplay.isLayoutNeeded() + && !mUpdateRotation)) { mService.checkDrawnWindowsLocked(); } @@ -925,7 +941,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> { for (DisplayContent displayContent : displayList) { mService.mLayersController.assignLayersLocked(displayContent.getWindowList()); - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); } } @@ -951,7 +967,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> { } // TODO: Super crazy long method that should be broken down... - void applySurfaceChangesTransaction(boolean recoveringMemory, int defaultDw, int defaultDh) { + private void applySurfaceChangesTransaction(boolean recoveringMemory, int defaultDw, int defaultDh) { mHoldScreenWindow = null; mObsuringWindow = null; @@ -993,7 +1009,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> { repeats++; if (repeats > 6) { Slog.w(TAG, "Animation repeat aborted after too many iterations"); - dc.layoutNeeded = false; + dc.clearLayoutNeeded(); break; } @@ -1003,20 +1019,20 @@ class RootWindowContainer extends WindowContainer<DisplayContent> { if ((dc.pendingLayoutChanges & FINISH_LAYOUT_REDO_WALLPAPER) != 0 && mService.mWallpaperControllerLocked.adjustWallpaperWindows()) { mService.mLayersController.assignLayersLocked(windows); - dc.layoutNeeded = true; + dc.setLayoutNeeded(); } if (isDefaultDisplay && (dc.pendingLayoutChanges & FINISH_LAYOUT_REDO_CONFIG) != 0) { if (DEBUG_LAYOUT) Slog.v(TAG, "Computing new config from layout"); if (mService.updateOrientationFromAppTokensLocked(true)) { - dc.layoutNeeded = true; + dc.setLayoutNeeded(); mService.mH.sendEmptyMessage(SEND_NEW_CONFIGURATION); } } if ((dc.pendingLayoutChanges & FINISH_LAYOUT_REDO_LAYOUT) != 0) { - dc.layoutNeeded = true; + dc.setLayoutNeeded(); } // FIRST LOOP: Perform a layout, if needed. @@ -1407,14 +1423,14 @@ class RootWindowContainer extends WindowContainer<DisplayContent> { } void dumpLayoutNeededDisplayIds(PrintWriter pw) { - if (!layoutNeeded()) { + if (!isLayoutNeeded()) { return; } - pw.print(" layoutNeeded on displays="); + pw.print(" mLayoutNeeded on displays="); final int count = mChildren.size(); for (int displayNdx = 0; displayNdx < count; ++displayNdx) { final DisplayContent displayContent = mChildren.get(displayNdx); - if (displayContent.layoutNeeded) { + if (displayContent.isLayoutNeeded()) { pw.print(displayContent.getDisplayId()); } } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 9e8c6091909e..f6598c14da44 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -70,12 +70,6 @@ class Task extends WindowContainer<AppWindowToken> implements DimLayer.DimLayerU // Whether mBounds is fullscreen private boolean mFillsParent = true; - /** - * Contains configurations settings that are different from the parent configuration due to - * stack specific operations. E.g. {@link #setBounds}. - */ - Configuration mOverrideConfig = Configuration.EMPTY; - // For comparison with DisplayContent bounds. private Rect mTmpRect = new Rect(); // For handling display rotations. @@ -120,8 +114,9 @@ class Task extends WindowContainer<AppWindowToken> implements DimLayer.DimLayerU } } - if (wtoken.mParent != null) { - wtoken.mParent.removeChild(wtoken); + final WindowContainer parent = wtoken.getParent(); + if (parent != null) { + parent.removeChild(wtoken); } addChild(wtoken, addPos); wtoken.mTask = this; @@ -153,7 +148,7 @@ class Task extends WindowContainer<AppWindowToken> implements DimLayer.DimLayerU if (content != null) { content.mDimLayerController.removeDimLayerUser(this); } - mParent.removeChild(this); + getParent().removeChild(this); mService.mTaskIdToTask.delete(mTaskId); } @@ -165,7 +160,7 @@ class Task extends WindowContainer<AppWindowToken> implements DimLayer.DimLayerU if (DEBUG_STACK) Slog.i(TAG, "moveTaskToStack: removing taskId=" + mTaskId + " from stack=" + mStack); EventLog.writeEvent(EventLogTags.WM_TASK_REMOVED, mTaskId, "moveTask"); - mParent.removeChild(this); + getParent().removeChild(this); stack.addTask(this, toTop); } @@ -254,7 +249,7 @@ class Task extends WindowContainer<AppWindowToken> implements DimLayer.DimLayerU if (displayContent != null) { displayContent.mDimLayerController.updateDimLayer(this); } - mOverrideConfig = mFillsParent ? Configuration.EMPTY : overrideConfig; + onOverrideConfigurationChanged(mFillsParent ? Configuration.EMPTY : overrideConfig); return boundsChange; } @@ -321,8 +316,7 @@ class Task extends WindowContainer<AppWindowToken> implements DimLayer.DimLayerU */ void prepareFreezingBounds() { mPreparedFrozenBounds.set(mBounds); - mPreparedFrozenMergedConfig.setTo(mService.mGlobalConfiguration); - mPreparedFrozenMergedConfig.updateFrom(mOverrideConfig); + mPreparedFrozenMergedConfig.setTo(getConfiguration()); } /** @@ -334,9 +328,9 @@ class Task extends WindowContainer<AppWindowToken> implements DimLayer.DimLayerU * bounds's bottom; false if the task's top should be aligned * the adjusted bounds's top. */ - void alignToAdjustedBounds( - Rect adjustedBounds, Rect tempInsetBounds, boolean alignBottom) { - if (!isResizeable() || mOverrideConfig == Configuration.EMPTY) { + void alignToAdjustedBounds(Rect adjustedBounds, Rect tempInsetBounds, boolean alignBottom) { + final Configuration overrideConfig = getOverrideConfiguration(); + if (!isResizeable() || Configuration.EMPTY.equals(overrideConfig)) { return; } @@ -348,7 +342,7 @@ class Task extends WindowContainer<AppWindowToken> implements DimLayer.DimLayerU mTmpRect2.offsetTo(adjustedBounds.left, adjustedBounds.top); } setTempInsetBounds(tempInsetBounds); - resizeLocked(mTmpRect2, mOverrideConfig, false /* forced */); + resizeLocked(mTmpRect2, overrideConfig, false /* forced */); } /** Return true if the current bound can get outputted to the rest of the system as-is. */ @@ -500,12 +494,12 @@ class Task extends WindowContainer<AppWindowToken> implements DimLayer.DimLayerU mTmpRect2.set(mBounds); if (!StackId.isTaskResizeAllowed(mStack.mStackId)) { - setBounds(mTmpRect2, mOverrideConfig); + setBounds(mTmpRect2, getOverrideConfiguration()); return; } displayContent.rotateBounds(mRotation, newRotation, mTmpRect2); - if (setBounds(mTmpRect2, mOverrideConfig) != BOUNDS_CHANGE_NONE) { + if (setBounds(mTmpRect2, getOverrideConfiguration()) != BOUNDS_CHANGE_NONE) { // Post message to inform activity manager of the bounds change simulating a one-way // call. We do this to prevent a deadlock between window manager lock and activity // manager lock been held. diff --git a/services/core/java/com/android/server/wm/TaskPositioner.java b/services/core/java/com/android/server/wm/TaskPositioner.java index 21db840ad255..688731256f89 100644 --- a/services/core/java/com/android/server/wm/TaskPositioner.java +++ b/services/core/java/com/android/server/wm/TaskPositioner.java @@ -478,7 +478,7 @@ class TaskPositioner implements DimLayer.DimLayerUser { private int getDimSide(int x) { if (mTask.mStack.mStackId != FREEFORM_WORKSPACE_STACK_ID || !mTask.mStack.fillsParent() - || mService.mGlobalConfiguration.orientation != ORIENTATION_LANDSCAPE) { + || mTask.mStack.getConfiguration().orientation != ORIENTATION_LANDSCAPE) { return CTRL_NONE; } diff --git a/services/core/java/com/android/server/wm/TaskStack.java b/services/core/java/com/android/server/wm/TaskStack.java index e374185e7210..e98fc393d404 100644 --- a/services/core/java/com/android/server/wm/TaskStack.java +++ b/services/core/java/com/android/server/wm/TaskStack.java @@ -230,7 +230,7 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye } } alignTasksToAdjustedBounds(adjusted ? mAdjustedBounds : mBounds, insetBounds); - mDisplayContent.layoutNeeded = true; + mDisplayContent.setLayoutNeeded(); } private void alignTasksToAdjustedBounds(Rect adjustedBounds, Rect tempInsetBounds) { @@ -354,11 +354,8 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye // If the rotation or density didn't match, we'll update it in onConfigurationChanged. } - boolean onConfigurationChanged() { - return updateBoundsAfterConfigChange(); - } - - private boolean updateBoundsAfterConfigChange() { + /** @return true if bounds were updated to some non-empty value. */ + boolean updateBoundsAfterConfigChange() { if (mDisplayContent == null) { // If the stack is already detached we're not updating anything, // as it's going away soon anyway. @@ -459,7 +456,7 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye // Snap the position to a target. final int rotation = displayInfo.rotation; - final int orientation = mService.mGlobalConfiguration.orientation; + final int orientation = mDisplayContent.getConfiguration().orientation; mService.mPolicy.getStableInsetsLw(rotation, displayWidth, displayHeight, outBounds); final DividerSnapAlgorithm algorithm = new DividerSnapAlgorithm( mService.mContext.getResources(), displayWidth, displayHeight, @@ -598,7 +595,7 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye if (mChildren.isEmpty()) { mDisplayContent.moveStack(this, false); } - mDisplayContent.layoutNeeded = true; + mDisplayContent.setLayoutNeeded(); } for (int appNdx = mExitingAppTokens.size() - 1; appNdx >= 0; --appNdx) { final AppWindowToken wtoken = mExitingAppTokens.get(appNdx); @@ -713,7 +710,7 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye di.logicalWidth, di.logicalHeight, dockDividerWidth, - mService.mGlobalConfiguration.orientation == ORIENTATION_PORTRAIT, + mDisplayContent.getConfiguration().orientation == ORIENTATION_PORTRAIT, mTmpRect2).getMiddleTarget().position; if (dockOnTopOrLeft) { @@ -1080,7 +1077,7 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye final Rect insetBounds = mImeGoingAway ? mBounds : mFullyAdjustedImeBounds; task.alignToAdjustedBounds(mAdjustedBounds, insetBounds, getDockSide() == DOCKED_TOP); - mDisplayContent.layoutNeeded = true; + mDisplayContent.setLayoutNeeded(); } boolean isAdjustedForMinimizedDockedStack() { @@ -1182,7 +1179,7 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye return DOCKED_INVALID; } mDisplayContent.getLogicalDisplayRect(mTmpRect); - final int orientation = mService.mGlobalConfiguration.orientation; + final int orientation = mDisplayContent.getConfiguration().orientation; return getDockSideUnchecked(bounds, mTmpRect, orientation); } diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index af0fbd3ba24a..8777d88a8777 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -17,6 +17,7 @@ package com.android.server.wm; import android.annotation.CallSuper; +import android.content.res.Configuration; import android.view.animation.Animation; import java.util.Comparator; @@ -34,13 +35,33 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; */ class WindowContainer<E extends WindowContainer> implements Comparable<WindowContainer> { - // The parent of this window container. - protected WindowContainer mParent = null; + /** + * The parent of this window container. + * For removing or setting new parent {@link #setParent} should be used, because it also + * performs configuration updates based on new parent's settings. + */ + private WindowContainer mParent = null; // List of children for this window container. List is in z-order as the children appear on // screen with the top-most window container at the tail of the list. protected final LinkedList<E> mChildren = new LinkedList(); + /** Contains override configuration settings applied to this window container. */ + private Configuration mOverrideConfiguration = new Configuration(); + + /** + * Contains full configuration applied to this window container. Corresponds to full parent's + * config with applied {@link #mOverrideConfiguration}. + */ + private Configuration mFullConfiguration = new Configuration(); + + /** + * Contains merged override configuration settings from the top of the hierarchy down to this + * particular instance. It is different from {@link #mFullConfiguration} because it starts from + * topmost container's override config instead of global config. + */ + private Configuration mMergedOverrideConfiguration = new Configuration(); + // The specified orientation for this window container. protected int mOrientation = SCREEN_ORIENTATION_UNSPECIFIED; @@ -48,6 +69,14 @@ class WindowContainer<E extends WindowContainer> implements Comparable<WindowCon return mParent; } + final protected void setParent(WindowContainer parent) { + mParent = parent; + // Update full configuration of this container and all its children. + onConfigurationChanged(mParent != null ? mParent.mFullConfiguration : Configuration.EMPTY); + // Update merged override configuration of this container and all its children. + onMergedOverrideConfigurationChanged(); + } + // Temp. holders for a chain of containers we are currently processing. private final LinkedList<WindowContainer> mTmpChain1 = new LinkedList(); private final LinkedList<WindowContainer> mTmpChain2 = new LinkedList(); @@ -61,12 +90,12 @@ class WindowContainer<E extends WindowContainer> implements Comparable<WindowCon */ @CallSuper protected void addChild(E child, Comparator<E> comparator) { - if (child.mParent != null) { + if (child.getParent() != null) { throw new IllegalArgumentException("addChild: container=" + child.getName() - + " is already a child of container=" + child.mParent.getName() + + " is already a child of container=" + child.getParent().getName() + " can't add to container=" + getName()); } - child.mParent = this; + child.setParent(this); if (mChildren.isEmpty() || comparator == null) { mChildren.add(child); @@ -87,12 +116,12 @@ class WindowContainer<E extends WindowContainer> implements Comparable<WindowCon /** Adds the input window container has a child of this container at the input index. */ @CallSuper protected void addChild(E child, int index) { - if (child.mParent != null) { + if (child.getParent() != null) { throw new IllegalArgumentException("addChild: container=" + child.getName() - + " is already a child of container=" + child.mParent.getName() + + " is already a child of container=" + child.getParent().getName() + " can't add to container=" + getName()); } - child.mParent = this; + child.setParent(this); mChildren.add(index, child); } @@ -104,7 +133,7 @@ class WindowContainer<E extends WindowContainer> implements Comparable<WindowCon @CallSuper void removeChild(E child) { if (mChildren.remove(child)) { - child.mParent = null; + child.setParent(null); } else { throw new IllegalArgumentException("removeChild: container=" + child.getName() + " is not a child of container=" + getName()); @@ -158,6 +187,73 @@ class WindowContainer<E extends WindowContainer> implements Comparable<WindowCon return false; } + /** + * Returns full configuration applied to this window container. + * This method should be used for getting settings applied in each particular level of the + * hierarchy. + */ + Configuration getConfiguration() { + return mFullConfiguration; + } + + /** + * Notify that parent config changed and we need to update full configuration. + * @see #mFullConfiguration + */ + void onConfigurationChanged(Configuration newParentConfig) { + mFullConfiguration.setTo(newParentConfig); + mFullConfiguration.updateFrom(mOverrideConfiguration); + for (int i = mChildren.size() - 1; i >= 0; --i) { + final WindowContainer child = mChildren.get(i); + child.onConfigurationChanged(mFullConfiguration); + } + } + + /** Returns override configuration applied to this window container. */ + Configuration getOverrideConfiguration() { + return mOverrideConfiguration; + } + + /** + * Update override configuration and recalculate full config. + * @see #mOverrideConfiguration + * @see #mFullConfiguration + */ + void onOverrideConfigurationChanged(Configuration overrideConfiguration) { + mOverrideConfiguration.setTo(overrideConfiguration); + // Update full configuration of this container and all its children. + onConfigurationChanged(mParent != null ? mParent.getConfiguration() : Configuration.EMPTY); + // Update merged override config of this container and all its children. + onMergedOverrideConfigurationChanged(); + } + + /** + * Get merged override configuration from the top of the hierarchy down to this + * particular instance. This should be reported to client as override config. + */ + Configuration getMergedOverrideConfiguration() { + return mMergedOverrideConfiguration; + } + + /** + * Update merged override configuration based on corresponding parent's config and notify all + * its children. If there is no parent, merged override configuration will set equal to current + * override config. + * @see #mMergedOverrideConfiguration + */ + private void onMergedOverrideConfigurationChanged() { + if (mParent != null) { + mMergedOverrideConfiguration.setTo(mParent.getMergedOverrideConfiguration()); + mMergedOverrideConfiguration.updateFrom(mOverrideConfiguration); + } else { + mMergedOverrideConfiguration.setTo(mOverrideConfiguration); + } + for (int i = mChildren.size() - 1; i >= 0; --i) { + final WindowContainer child = mChildren.get(i); + child.onMergedOverrideConfigurationChanged(); + } + } + void setWaitingForDrawnIfResizingChanged() { for (int i = mChildren.size() - 1; i >= 0; --i) { final WindowContainer wc = mChildren.get(i); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 1ed4055746b8..310ad5a46aea 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -592,12 +592,6 @@ public class WindowManagerService extends IWindowManager.Stub // State while inside of layoutAndPlaceSurfacesLocked(). boolean mFocusMayChange; - /** - * Current global configuration information. Contains general settings for the entire system, - * corresponds to the configuration of the default display. - */ - Configuration mGlobalConfiguration = new Configuration(); - // This is held as long as we have the screen frozen, to give us time to // perform a rotation animation when turning off shows the lock screen which // changes the orientation. @@ -2674,7 +2668,8 @@ public class WindowManagerService extends IWindowManager.Stub // is running. Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "WM#applyAnimationLocked"); if (okToDisplay()) { - DisplayInfo displayInfo = getDefaultDisplayInfoLocked(); + final DisplayContent displayContent = atoken.mTask.getDisplayContent(); + final DisplayInfo displayInfo = displayContent.getDisplayInfo(); final int width = displayInfo.appWidth; final int height = displayInfo.appHeight; if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG_WM, @@ -2711,10 +2706,10 @@ public class WindowManagerService extends IWindowManager.Stub if (DEBUG_APP_TRANSITIONS) Slog.d(TAG_WM, "Loading animation for app transition." + " transit=" + AppTransition.appTransitionToString(transit) + " enter=" + enter + " frame=" + frame + " insets=" + insets + " surfaceInsets=" + surfaceInsets); - Animation a = mAppTransition.loadAnimation(lp, transit, enter, - mGlobalConfiguration.uiMode, mGlobalConfiguration.orientation, frame, - displayFrame, insets, surfaceInsets, isVoiceInteraction, freeform, - atoken.mTask.mTaskId); + final Configuration displayConfig = displayContent.getConfiguration(); + Animation a = mAppTransition.loadAnimation(lp, transit, enter, displayConfig.uiMode, + displayConfig.orientation, frame, displayFrame, insets, surfaceInsets, + isVoiceInteraction, freeform, atoken.mTask.mTaskId); if (a != null) { if (DEBUG_ANIM) logWithStack(TAG, "Loaded animation " + a + " for " + atoken); final int containingWidth = frame.width(); @@ -3029,7 +3024,7 @@ public class WindowManagerService extends IWindowManager.Stub if (currentConfig.diff(mTempConfiguration) != 0) { mWaitingForConfig = true; final DisplayContent displayContent = getDefaultDisplayContentLocked(); - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); int anim[] = new int[2]; if (displayContent.isDimming()) { anim[0] = anim[1] = 0; @@ -3104,11 +3099,7 @@ public class WindowManagerService extends IWindowManager.Stub mWaitingForConfig = false; mLastFinishedFreezeSource = "new-config"; } - final boolean configChanged = mGlobalConfiguration.diff(config) != 0; - if (!configChanged) { - return null; - } - return mRoot.onConfigurationChanged(config); + return mRoot.setGlobalConfigurationIfNeeded(config); } } @@ -3804,7 +3795,7 @@ public class WindowManagerService extends IWindowManager.Stub displayContent.rebuildAppWindowList(); - // Set displayContent.layoutNeeded if window order changed. + // Set displayContent.mLayoutNeeded if window order changed. final int tmpSize = mTmpWindows.size(); final int winSize = windows.size(); int tmpNdx = 0, winNdx = 0; @@ -3822,13 +3813,13 @@ public class WindowManagerService extends IWindowManager.Stub if (tmp != win) { // Window order changed. - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); break; } } if (tmpNdx != winNdx) { // One list was different from the other. - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); } mTmpWindows.clear(); @@ -3992,7 +3983,7 @@ public class WindowManagerService extends IWindowManager.Stub } task.moveTaskToStack(stack, toTop); final DisplayContent displayContent = stack.getDisplayContent(); - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); mWindowPlacerLocked.performSurfacePlacement(); } } @@ -4044,7 +4035,7 @@ public class WindowManagerService extends IWindowManager.Stub } if (stack.setBounds(bounds, configs, taskBounds, taskTempInsetBounds) && stack.isVisible()) { - stack.getDisplayContent().layoutNeeded = true; + stack.getDisplayContent().setLayoutNeeded(); mWindowPlacerLocked.performSurfacePlacement(); } return stack.getRawFullscreen(); @@ -4081,7 +4072,7 @@ public class WindowManagerService extends IWindowManager.Stub } task.positionTaskInStack(stack, position, bounds, config); final DisplayContent displayContent = stack.getDisplayContent(); - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); mWindowPlacerLocked.performSurfacePlacement(); } } @@ -4101,7 +4092,7 @@ public class WindowManagerService extends IWindowManager.Stub } if (task.resizeLocked(bounds, overrideConfig, forced) && relayout) { - task.getDisplayContent().layoutNeeded = true; + task.getDisplayContent().setLayoutNeeded(); mWindowPlacerLocked.performSurfacePlacement(); } } @@ -5459,7 +5450,7 @@ public class WindowManagerService extends IWindowManager.Stub synchronized(mWindowMap) { changed = updateRotationUncheckedLocked(false); if (!changed || forceRelayout) { - getDefaultDisplayContentLocked().layoutNeeded = true; + getDefaultDisplayContentLocked().setLayoutNeeded(); mWindowPlacerLocked.performSurfacePlacement(); } } @@ -5542,7 +5533,7 @@ public class WindowManagerService extends IWindowManager.Stub mH.sendEmptyMessageDelayed(H.WINDOW_FREEZE_TIMEOUT, WINDOW_FREEZE_TIMEOUT_DURATION); mWaitingForConfig = true; final DisplayContent displayContent = getDefaultDisplayContentLocked(); - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); final int[] anim = new int[2]; if (displayContent.isDimming()) { anim[0] = anim[1] = 0; @@ -5595,7 +5586,7 @@ public class WindowManagerService extends IWindowManager.Stub // the top of the method, the caller is obligated to call computeNewConfigurationLocked(). // By updating the Display info here it will be available to // computeScreenConfigurationLocked later. - updateDisplayAndOrientationLocked(mGlobalConfiguration.uiMode); + updateDisplayAndOrientationLocked(mRoot.getConfiguration().uiMode); final DisplayInfo displayInfo = displayContent.getDisplayInfo(); if (!inTransaction) { @@ -6961,8 +6952,8 @@ public class WindowManagerService extends IWindowManager.Stub View view = null; try { - final Configuration overrideConfig = wtoken != null && wtoken.mTask != null - ? wtoken.mTask.mOverrideConfig : null; + final Configuration overrideConfig = + wtoken != null ? wtoken.getMergedOverrideConfiguration() : null; view = mPolicy.addStartingWindow(wtoken.token, sd.pkg, sd.theme, sd.compatInfo, sd.nonLocalizedLabel, sd.labelRes, sd.icon, sd.logo, sd.windowFlags, overrideConfig); @@ -7908,12 +7899,13 @@ public class WindowManagerService extends IWindowManager.Stub return; } configureDisplayPolicyLocked(displayContent); - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); boolean configChanged = updateOrientationFromAppTokensLocked(false); - mTempConfiguration.setTo(mGlobalConfiguration); + final Configuration globalConfig = mRoot.getConfiguration(); + mTempConfiguration.setTo(globalConfig); computeScreenConfigurationLocked(mTempConfiguration); - configChanged |= mGlobalConfiguration.diff(mTempConfiguration) != 0; + configChanged |= globalConfig.diff(mTempConfiguration) != 0; if (configChanged) { mWaitingForConfig = true; @@ -8071,7 +8063,7 @@ public class WindowManagerService extends IWindowManager.Stub w.setReportResizeHints(); boolean configChanged = w.isConfigChanged(); if (DEBUG_CONFIGURATION && configChanged) { - Slog.v(TAG_WM, "Win " + w + " config changed: " + mGlobalConfiguration); + Slog.v(TAG_WM, "Win " + w + " config changed: " + w.getConfiguration()); } final boolean dragResizingChanged = w.isDragResizeChanged() && !w.isDragResizingChangeReported(); @@ -8259,7 +8251,7 @@ public class WindowManagerService extends IWindowManager.Stub mode != UPDATE_FOCUS_WILL_ASSIGN_LAYERS && mode != UPDATE_FOCUS_WILL_PLACE_SURFACES); if (imWindowChanged) { - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); newFocus = mRoot.computeFocusedWindow(); } @@ -8286,7 +8278,7 @@ public class WindowManagerService extends IWindowManager.Stub if ((focusChanged & WindowManagerPolicy.FINISH_LAYOUT_REDO_LAYOUT) != 0) { // The change in focus caused us to need to do a layout. Okay. - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); if (mode == UPDATE_FOCUS_PLACING_SURFACES) { mWindowPlacerLocked.performLayoutLockedInner(displayContent, true /*initial*/, updateInputWindows); @@ -8959,7 +8951,7 @@ public class WindowManagerService extends IWindowManager.Stub } } pw.println(); - pw.print(" mGlobalConfiguration="); pw.println(mGlobalConfiguration); + pw.print(" mGlobalConfiguration="); pw.println(mRoot.getConfiguration()); pw.print(" mHasPermanentDpad="); pw.println(mHasPermanentDpad); pw.print(" mCurrentFocus="); pw.println(mCurrentFocus); if (mLastFocus != mCurrentFocus) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index c3affeb9a25c..ee2da1375998 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1493,7 +1493,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } changed = true; if (displayContent != null) { - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); } } @@ -2039,7 +2039,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP void setDisplayLayoutNeeded() { if (mDisplayContent != null) { - mDisplayContent.layoutNeeded = true; + mDisplayContent.setLayoutNeeded(); } } @@ -2886,14 +2886,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP outConfig.setTo(mAppToken.mFrozenMergedConfig.peek()); return; } - final Task task = getTask(); - final Configuration overrideConfig = task != null - ? task.mOverrideConfig - : Configuration.EMPTY; - outConfig.setTo(mService.mGlobalConfiguration); - if (overrideConfig != Configuration.EMPTY) { - outConfig.updateFrom(overrideConfig); - } + outConfig.setTo( + mAppToken != null ? getConfiguration() : mDisplayContent.getConfiguration()); } void reportResized() { @@ -3528,7 +3522,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP void requestUpdateWallpaperIfNeeded() { if (mDisplayContent != null && (mAttrs.flags & FLAG_SHOW_WALLPAPER) != 0) { mDisplayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER; - mDisplayContent.layoutNeeded = true; + mDisplayContent.setLayoutNeeded(); mService.mWindowPlacerLocked.requestTraversal(); } @@ -3646,7 +3640,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // want to make sure to do a layout. If called from within the transaction // loop, this will cause it to restart with a new layout. if (displayContent != null) { - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); } } } diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index cbb5040de18a..6c7d1362071a 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -482,7 +482,7 @@ class WindowStateAnimator { // Upon completion of a not-visible to visible status bar animation a relayout is // required. if (displayContent != null) { - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); } } diff --git a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java index 6d10c5aaf34e..668e1b4ec011 100644 --- a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java +++ b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java @@ -25,6 +25,7 @@ import static com.android.server.wm.WindowManagerService.LAYOUT_REPEAT_THRESHOLD import static com.android.server.wm.WindowManagerService.MAX_ANIMATION_DURATION; import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_PLACING_SURFACES; +import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.PixelFormat; @@ -168,7 +169,7 @@ class WindowSurfacePlacer { mInLayout = false; - if (mService.mRoot.layoutNeeded()) { + if (mService.mRoot.isLayoutNeeded()) { if (++mLayoutRepeatCount < 6) { requestTraversal(); } else { @@ -204,10 +205,10 @@ class WindowSurfacePlacer { final void performLayoutLockedInner(final DisplayContent displayContent, boolean initial, boolean updateInputWindows) { - if (!displayContent.layoutNeeded) { + if (!displayContent.isLayoutNeeded()) { return; } - displayContent.layoutNeeded = false; + displayContent.clearLayoutNeeded(); WindowList windows = displayContent.getWindowList(); boolean isDefaultDisplay = displayContent.isDefaultDisplay; @@ -228,12 +229,12 @@ class WindowSurfacePlacer { if (DEBUG_LAYOUT) { Slog.v(TAG, "-------------------------------------"); - Slog.v(TAG, "performLayout: needed=" - + displayContent.layoutNeeded + " dw=" + dw + " dh=" + dh); + Slog.v(TAG, "performLayout: needed=" + displayContent.isLayoutNeeded() + + " dw=" + dw + " dh=" + dh); } mService.mPolicy.beginLayoutLw(isDefaultDisplay, dw, dh, mService.mRotation, - mService.mGlobalConfiguration.uiMode); + displayContent.getConfiguration().uiMode); if (isDefaultDisplay) { // Not needed on non-default displays. mService.mSystemDecorLayer = mService.mPolicy.getSystemDecorLayerLw(); @@ -427,7 +428,7 @@ class WindowSurfacePlacer { if ((displayContent.pendingLayoutChanges & FINISH_LAYOUT_REDO_WALLPAPER) != 0 && mWallpaperControllerLocked.adjustWallpaperWindows()) { mService.mLayersController.assignLayersLocked(windows); - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); } final WindowState lowerWallpaperTarget = @@ -532,7 +533,7 @@ class WindowSurfacePlacer { // This has changed the visibility of windows, so perform // a new layout to get them all up-to-date. - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); // TODO(multidisplay): IMEs are only supported on the default display. if (windows == mService.getDefaultWindowListLocked() @@ -840,13 +841,14 @@ class WindowSurfacePlacer { Rect appRect = win != null ? win.getContentFrameLw() : new Rect(0, 0, displayInfo.appWidth, displayInfo.appHeight); Rect insets = win != null ? win.mContentInsets : null; + final Configuration displayConfig = displayContent.getConfiguration(); // For the new aspect-scaled transition, we want it to always show // above the animating opening/closing window, and we want to // synchronize its thumbnail surface with the surface for the // open/close animation (only on the way down) anim = mService.mAppTransition.createThumbnailAspectScaleAnimationLocked(appRect, - insets, thumbnailHeader, taskId, mService.mGlobalConfiguration.uiMode, - mService.mGlobalConfiguration.orientation); + insets, thumbnailHeader, taskId, displayConfig.uiMode, + displayConfig.orientation); openingAppAnimator.thumbnailForceAboveLayer = Math.max(openingLayer, closingLayer); openingAppAnimator.deferThumbnailDestruction = !mService.mAppTransition.isNextThumbnailTransitionScaleUp(); diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java index 7ed8e7844e0b..177652c21bdc 100644 --- a/services/core/java/com/android/server/wm/WindowToken.java +++ b/services/core/java/com/android/server/wm/WindowToken.java @@ -16,7 +16,6 @@ package com.android.server.wm; -import android.annotation.CallSuper; import android.os.Bundle; import android.os.Debug; import android.os.IBinder; @@ -258,7 +257,7 @@ class WindowToken extends WindowContainer<WindowState> { if (hidden == visible) { hidden = !visible; // Need to do a layout to ensure the wallpaper now has the correct size. - displayContent.layoutNeeded = true; + displayContent.setLayoutNeeded(); } final WallpaperController wallpaperController = mService.mWallpaperControllerLocked; @@ -281,7 +280,7 @@ class WindowToken extends WindowContainer<WindowState> { "Wallpaper token " + token + " hidden=" + !visible); hidden = !visible; // Need to do a layout to ensure the wallpaper now has the correct size. - mService.getDefaultDisplayContentLocked().layoutNeeded = true; + mService.getDefaultDisplayContentLocked().setLayoutNeeded(); } final WallpaperController wallpaperController = mService.mWallpaperControllerLocked; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 3d0ca8097f02..0c57179f94b2 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -496,6 +496,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } }); } + // STOPSHIP: Remove this code once all dogfood devices are fixed. See b/31754835 + if (Intent.ACTION_BOOT_COMPLETED.equals(action) && !mOwners.hasDeviceOwner() + && !isBackupServiceEnabledInternal()) { + setBackupServiceEnabledInternal(true); + Slog.w(LOG_TAG, "Fix backup for device that is not in Device Owner mode."); + } if (Intent.ACTION_USER_UNLOCKED.equals(action) || Intent.ACTION_USER_STARTED.equals(action) || KeyChain.ACTION_TRUST_STORE_CHANGED.equals(action)) { @@ -9058,8 +9064,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (!isDeviceOwnerManagedSingleUserDevice()) { mInjector.securityLogSetLoggingEnabledProperty(false); Slog.w(LOG_TAG, "Security logging turned off as it's no longer a single user device."); - setBackupServiceEnabledInternal(false); - Slog.w(LOG_TAG, "Backup is off as it's a managed device that has more that one user."); + if (mOwners.hasDeviceOwner()) { + setBackupServiceEnabledInternal(false); + Slog.w(LOG_TAG, "Backup is off as it's a managed device that has more that one user."); + } } } @@ -9350,12 +9358,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } synchronized (this) { getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER); - try { - IBackupManager ibm = mInjector.getIBackupManager(); - return ibm != null && ibm.isBackupServiceActive(UserHandle.USER_SYSTEM); - } catch (RemoteException e) { - throw new IllegalStateException("Failed requesting backup service state.", e); - } + return isBackupServiceEnabledInternal(); + } + } + private boolean isBackupServiceEnabledInternal() { + try { + IBackupManager ibm = mInjector.getIBackupManager(); + return ibm != null && ibm.isBackupServiceActive(UserHandle.USER_SYSTEM); + } catch (RemoteException e) { + throw new IllegalStateException("Failed requesting backup service state.", e); } } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index aba4dc0c633a..769b5ee92997 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -859,12 +859,7 @@ public final class SystemServer { if (!disableNonCoreServices) { traceBeginAndSlog("StartClipboardService"); - try { - ServiceManager.addService(Context.CLIPBOARD_SERVICE, - new ClipboardService(context)); - } catch (Throwable e) { - reportWtf("starting Clipboard Service", e); - } + mSystemServiceManager.startService(ClipboardService.class); traceEnd(); } @@ -1372,12 +1367,9 @@ public final class SystemServer { mmsService = mSystemServiceManager.startService(MmsServiceBroker.class); traceEnd(); - if (Settings.Global.getInt(mContentResolver, Settings.Global.DEVICE_PROVISIONED, 0) == 0 || - UserManager.isDeviceInDemoMode(mSystemContext)) { - traceBeginAndSlog("StartRetailDemoModeService"); - mSystemServiceManager.startService(RetailDemoModeService.class); - traceEnd(); - } + traceBeginAndSlog("StartRetailDemoModeService"); + mSystemServiceManager.startService(RetailDemoModeService.class); + traceEnd(); // It is now time to start up the app processes... diff --git a/services/net/java/android/net/ip/IpManager.java b/services/net/java/android/net/ip/IpManager.java index 215059d563f6..ee67d953ec29 100644 --- a/services/net/java/android/net/ip/IpManager.java +++ b/services/net/java/android/net/ip/IpManager.java @@ -358,6 +358,7 @@ public class IpManager extends StateMachine { } public static final String DUMP_ARG = "ipmanager"; + public static final String DUMP_ARG_CONFIRM = "confirm"; private static final int CMD_STOP = 1; private static final int CMD_START = 2; @@ -562,6 +563,12 @@ public class IpManager extends StateMachine { } public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + if (args.length > 0 && DUMP_ARG_CONFIRM.equals(args[0])) { + // Execute confirmConfiguration() and take no further action. + confirmConfiguration(); + return; + } + IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); pw.println("APF dump:"); pw.increaseIndent(); diff --git a/services/retaildemo/java/com/android/server/retaildemo/RetailDemoModeService.java b/services/retaildemo/java/com/android/server/retaildemo/RetailDemoModeService.java index f97e5574d5b3..7c7c299710f0 100644 --- a/services/retaildemo/java/com/android/server/retaildemo/RetailDemoModeService.java +++ b/services/retaildemo/java/com/android/server/retaildemo/RetailDemoModeService.java @@ -124,7 +124,12 @@ public class RetailDemoModeService extends SystemService { @GuardedBy("mActivityLock") long mLastUserActivityTime; - private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + private boolean mSafeBootRestrictionInitialState; + private int mPackageVerifierEnableInitialState; + + private IntentReceiver mBroadcastReceiver = null; + + private final class IntentReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (!mDeviceInDemoMode) { @@ -150,6 +155,9 @@ public class RetailDemoModeService extends SystemService { @Override public void handleMessage(Message msg) { + if (!mDeviceInDemoMode) { + return; + } switch (msg.what) { case MSG_TURN_SCREEN_ON: if (mInjector.isWakeLockHeld()) { @@ -219,7 +227,7 @@ public class RetailDemoModeService extends SystemService { if (mDeviceDemoModeUri.equals(uri)) { mDeviceInDemoMode = UserManager.isDeviceInDemoMode(getContext()); if (mDeviceInDemoMode) { - putDeviceInDemoMode(); + startDemoMode(); } else { mInjector.systemPropertiesSet(SYSTEM_PROPERTY_RETAIL_DEMO_ENABLED, "0"); if (mInjector.isWakeLockHeld()) { @@ -238,6 +246,7 @@ public class RetailDemoModeService extends SystemService { } } }); + stopDemoMode(); } } @@ -376,10 +385,20 @@ public class RetailDemoModeService extends SystemService { } private void registerBroadcastReceiver() { - final IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_SCREEN_OFF); - filter.addAction(ACTION_RESET_DEMO); - getContext().registerReceiver(mBroadcastReceiver, filter); + if (mBroadcastReceiver == null) { + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(ACTION_RESET_DEMO); + mBroadcastReceiver = new IntentReceiver(); + getContext().registerReceiver(mBroadcastReceiver, filter); + } + } + + private void unregisterBroadcastReceiver() { + if (mBroadcastReceiver != null) { + getContext().unregisterReceiver(mBroadcastReceiver); + mBroadcastReceiver = null; + } } private String[] getCameraIdsWithFlash() { @@ -407,9 +426,33 @@ public class RetailDemoModeService extends SystemService { } } - private void putDeviceInDemoMode() { + private void startDemoMode() { + mPreloadAppsInstaller = mInjector.getPreloadAppsInstaller(); + mInjector.initializeWakeLock(); + if (mCameraIdsWithFlash == null) { + mCameraIdsWithFlash = getCameraIdsWithFlash(); + } + registerBroadcastReceiver(); + mInjector.systemPropertiesSet(SYSTEM_PROPERTY_RETAIL_DEMO_ENABLED, "1"); mHandler.sendEmptyMessage(MSG_START_NEW_SESSION); + + mSafeBootRestrictionInitialState = mInjector.getUserManager().hasUserRestriction( + UserManager.DISALLOW_SAFE_BOOT, UserHandle.SYSTEM); + mPackageVerifierEnableInitialState = Settings.Global.getInt(mInjector.getContentResolver(), + Settings.Global.PACKAGE_VERIFIER_ENABLE, 0); + } + + private void stopDemoMode() { + mPreloadAppsInstaller = null; + mCameraIdsWithFlash = null; + mInjector.destroyWakeLock(); + unregisterBroadcastReceiver(); + + mInjector.getUserManager().setUserRestriction(UserManager.DISALLOW_SAFE_BOOT, + mSafeBootRestrictionInitialState, UserHandle.SYSTEM); + Settings.Global.putInt(mInjector.getContentResolver(), + Settings.Global.PACKAGE_VERIFIER_ENABLE, mPackageVerifierEnableInitialState); } @Override @@ -421,25 +464,21 @@ public class RetailDemoModeService extends SystemService { false); mHandlerThread.start(); mHandler = new MainHandler(mHandlerThread.getLooper()); - publishLocalService(RetailDemoModeServiceInternal.class, mLocalService); + mInjector.publishLocalService(this, mLocalService); } @Override public void onBootPhase(int bootPhase) { switch (bootPhase) { case PHASE_THIRD_PARTY_APPS_CAN_START: - mPreloadAppsInstaller = mInjector.getPreloadAppsInstaller(); - mInjector.initializeWakeLock(); - mCameraIdsWithFlash = getCameraIdsWithFlash(); SettingsObserver settingsObserver = new SettingsObserver(mHandler); settingsObserver.register(); settingsObserver.refreshTimeoutConstants(); - registerBroadcastReceiver(); break; case PHASE_BOOT_COMPLETED: if (UserManager.isDeviceInDemoMode(getContext())) { mDeviceInDemoMode = true; - putDeviceInDemoMode(); + startDemoMode(); } break; } @@ -466,8 +505,8 @@ public class RetailDemoModeService extends SystemService { mInjector.getSystemUsersConfiguration(), userId); mInjector.turnOffAllFlashLights(mCameraIdsWithFlash); muteVolumeStreams(); - if (!mInjector.isWifiEnabled()) { - mInjector.enableWifi(); + if (!mInjector.getWifiManager().isWifiEnabled()) { + mInjector.getWifiManager().setWifiEnabled(true); } // Disable lock screen for demo users. mInjector.getLockPatternUtils().setLockScreenDisabled(true, userId); @@ -526,6 +565,7 @@ public class RetailDemoModeService extends SystemService { private WifiManager mWifiManager; private Configuration mSystemUserConfiguration; private PendingIntent mResetDemoPendingIntent; + private PreloadAppsInstaller mPreloadAppsInstaller; Injector(Context context) { mContext = context; @@ -535,7 +575,7 @@ public class RetailDemoModeService extends SystemService { return mContext; } - private WifiManager getWifiManager() { + WifiManager getWifiManager() { if (mWifiManager == null) { mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); } @@ -609,7 +649,10 @@ public class RetailDemoModeService extends SystemService { } PreloadAppsInstaller getPreloadAppsInstaller() { - return new PreloadAppsInstaller(getContext()); + if (mPreloadAppsInstaller == null) { + mPreloadAppsInstaller = new PreloadAppsInstaller(getContext()); + } + return mPreloadAppsInstaller; } void systemPropertiesSet(String key, String value) { @@ -628,8 +671,14 @@ public class RetailDemoModeService extends SystemService { } void initializeWakeLock() { - mWakeLock = getPowerManager().newWakeLock( - PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG); + if (mWakeLock == null) { + mWakeLock = getPowerManager().newWakeLock( + PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG); + } + } + + void destroyWakeLock() { + mWakeLock = null; } boolean isWakeLockHeld() { @@ -644,14 +693,6 @@ public class RetailDemoModeService extends SystemService { mWakeLock.release(); } - boolean isWifiEnabled() { - return getWifiManager().isWifiEnabled(); - } - - void enableWifi() { - getWifiManager().setWifiEnabled(true); - } - void logSessionDuration(int duration) { MetricsLogger.histogram(getContext(), DEMO_SESSION_DURATION, duration); } @@ -696,5 +737,10 @@ public class RetailDemoModeService extends SystemService { File getDataPreloadsDirectory() { return Environment.getDataPreloadsDirectory(); } + + void publishLocalService(RetailDemoModeService service, + RetailDemoModeServiceInternal localService) { + service.publishLocalService(RetailDemoModeServiceInternal.class, localService); + } } } diff --git a/services/tests/servicestests/Android.mk b/services/tests/servicestests/Android.mk index b76392c967f6..3f5b96ea6a19 100644 --- a/services/tests/servicestests/Android.mk +++ b/services/tests/servicestests/Android.mk @@ -53,6 +53,8 @@ ifeq (true,$(EMMA_INSTRUMENT)) LOCAL_JACK_FLAGS := --multi-dex native endif # EMMA_INSTRUMENT_STATIC +LOCAL_STATIC_JAVA_LIBRARIES += ub-uiautomator + include $(BUILD_PACKAGE) ######################################################################### diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index b8ace28fc093..514f095cbd58 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -43,6 +43,7 @@ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-permission android:name="android.permission.PACKET_KEEPALIVE_OFFLOAD" /> <uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT" /> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS" /> <application> <uses-library android:name="android.test.runner" /> @@ -155,6 +156,9 @@ </intent-filter> </activity-alias> + <activity android:name="com.android.server.am.TaskStackChangedListenerTest$ActivityA" /> + <activity android:name="com.android.server.am.TaskStackChangedListenerTest$ActivityB" /> + </application> <instrumentation diff --git a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java index 340be6222173..257341b72a28 100644 --- a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java @@ -596,7 +596,7 @@ public class ConnectivityServiceTest extends AndroidTestCase { @Override protected CaptivePortalProbeResult isCaptivePortal() { - return new CaptivePortalProbeResult(gen204ProbeResult, gen204ProbeRedirectUrl); + return new CaptivePortalProbeResult(gen204ProbeResult, gen204ProbeRedirectUrl, null); } } diff --git a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java index e72a8dc45f04..9c241d759a3e 100644 --- a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java @@ -16,6 +16,8 @@ package com.android.server.accounts; +import static android.database.sqlite.SQLiteDatabase.deleteDatabase; +import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -24,7 +26,7 @@ import android.accounts.Account; import android.accounts.AccountManagerInternal; import android.accounts.AuthenticatorDescription; import android.app.AppOpsManager; -import android.app.Notification; +import android.app.INotificationManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -38,16 +40,14 @@ import android.database.DatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; import android.os.UserHandle; import android.os.UserManager; import android.test.AndroidTestCase; import android.test.mock.MockContext; -import android.test.mock.MockPackageManager; import android.test.suitebuilder.annotation.SmallTest; import android.util.Log; -import com.android.server.LocalServices; - import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -55,29 +55,38 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; public class AccountManagerServiceTest extends AndroidTestCase { private static final String TAG = AccountManagerServiceTest.class.getSimpleName(); - static final String PREN_DB = "pren.db"; - static final String DE_DB = "de.db"; - static final String CE_DB = "ce.db"; + private static final String PREN_DB = "pren.db"; + private static final String DE_DB = "de.db"; + private static final String CE_DB = "ce.db"; private AccountManagerService mAms; + private TestInjector mTestInjector; @Override protected void setUp() throws Exception { Context realTestContext = getContext(); - Context mockContext = new MyMockContext(realTestContext); + MyMockContext mockContext = new MyMockContext(realTestContext); setContext(mockContext); - mAms = createAccountManagerService(mockContext, realTestContext); + mTestInjector = new TestInjector(realTestContext, mockContext); + mAms = new AccountManagerService(mTestInjector); } @Override protected void tearDown() throws Exception { - SQLiteDatabase.deleteDatabase(new File(mAms.getCeDatabaseName(UserHandle.USER_SYSTEM))); - SQLiteDatabase.deleteDatabase(new File(mAms.getDeDatabaseName(UserHandle.USER_SYSTEM))); - SQLiteDatabase.deleteDatabase(new File(mAms.getPreNDatabaseName(UserHandle.USER_SYSTEM))); - LocalServices.removeServiceForTest(AccountManagerInternal.class); + // Let async logging tasks finish, otherwise they may crash due to db being removed + CountDownLatch cdl = new CountDownLatch(1); + mAms.mHandler.post(() -> { + deleteDatabase(new File(mTestInjector.getCeDatabaseName(UserHandle.USER_SYSTEM))); + deleteDatabase(new File(mTestInjector.getDeDatabaseName(UserHandle.USER_SYSTEM))); + deleteDatabase(new File(mTestInjector.getPreNDatabaseName(UserHandle.USER_SYSTEM))); + cdl.countDown(); + }); + cdl.await(1, TimeUnit.SECONDS); super.tearDown(); } @@ -230,7 +239,7 @@ public class AccountManagerServiceTest extends AndroidTestCase { Context originalContext = ((MyMockContext)getContext()).mTestContext; // create a separate instance of AMS. It initially assumes that user0 is locked - AccountManagerService ams2 = createAccountManagerService(getContext(), originalContext); + AccountManagerService ams2 = new AccountManagerService(mTestInjector); // Verify that account can be removed when user is locked ams2.removeAccountInternal(a1); @@ -239,7 +248,7 @@ public class AccountManagerServiceTest extends AndroidTestCase { assertEquals("Only a2 should be returned", a2, accounts[0]); // Verify that CE db file is unchanged and still has 2 accounts - String ceDatabaseName = mAms.getCeDatabaseName(UserHandle.USER_SYSTEM); + String ceDatabaseName = mTestInjector.getCeDatabaseName(UserHandle.USER_SYSTEM); int accountsNumber = readNumberOfAccountsFromDbFile(originalContext, ceDatabaseName); assertEquals("CE database should still have 2 accounts", 2, accountsNumber); @@ -254,7 +263,7 @@ public class AccountManagerServiceTest extends AndroidTestCase { @SmallTest public void testPreNDatabaseMigration() throws Exception { - String preNDatabaseName = mAms.getPreNDatabaseName(UserHandle.USER_SYSTEM); + String preNDatabaseName = mTestInjector.getPreNDatabaseName(UserHandle.USER_SYSTEM); Context originalContext = ((MyMockContext) getContext()).mTestContext; PreNTestDatabaseHelper.createV4Database(originalContext, preNDatabaseName); // Assert that database was created with 1 account @@ -275,8 +284,8 @@ public class AccountManagerServiceTest extends AndroidTestCase { new File(preNDatabaseName).exists()); // Verify that ce/de files are present - String deDatabaseName = mAms.getDeDatabaseName(UserHandle.USER_SYSTEM); - String ceDatabaseName = mAms.getCeDatabaseName(UserHandle.USER_SYSTEM); + String deDatabaseName = mTestInjector.getDeDatabaseName(UserHandle.USER_SYSTEM); + String ceDatabaseName = mTestInjector.getCeDatabaseName(UserHandle.USER_SYSTEM); assertTrue("DE database file should be created at " + deDatabaseName, new File(deDatabaseName).exists()); assertTrue("CE database file should be created at " + ceDatabaseName, @@ -291,13 +300,6 @@ public class AccountManagerServiceTest extends AndroidTestCase { } } - private AccountManagerService createAccountManagerService(Context mockContext, - Context realContext) { - LocalServices.removeServiceForTest(AccountManagerInternal.class); - return new MyAccountManagerService(mockContext, - new MyMockPackageManager(), new MockAccountAuthenticatorCache(), realContext); - } - private void unlockSystemUser() { mAms.onUserUnlocked(newIntentForUser(UserHandle.USER_SYSTEM)); } @@ -366,6 +368,8 @@ public class AccountManagerServiceTest extends AndroidTestCase { this.mAppOpsManager = mock(AppOpsManager.class); this.mUserManager = mock(UserManager.class); this.mPackageManager = mock(PackageManager.class); + when(mPackageManager.checkSignatures(anyInt(), anyInt())) + .thenReturn(PackageManager.SIGNATURE_MATCH); final UserInfo ui = new UserInfo(UserHandle.USER_SYSTEM, "user0", 0); when(mUserManager.getUserInfo(eq(ui.id))).thenReturn(ui); } @@ -381,6 +385,11 @@ public class AccountManagerServiceTest extends AndroidTestCase { } @Override + public String getPackageName() { + return mTestContext.getPackageName(); + } + + @Override public Object getSystemService(String name) { if (Context.APP_OPS_SERVICE.equals(name)) { return mAppOpsManager; @@ -427,47 +436,45 @@ public class AccountManagerServiceTest extends AndroidTestCase { } } - static class MyMockPackageManager extends MockPackageManager { - @Override - public int checkSignatures(final int uid1, final int uid2) { - return PackageManager.SIGNATURE_MATCH; + static class TestInjector extends AccountManagerService.Injector { + private Context mRealContext; + TestInjector(Context realContext, MyMockContext mockContext) { + super(mockContext); + mRealContext = realContext; } @Override - public void addOnPermissionsChangeListener( - OnPermissionsChangedListener listener) { - } - } - - static class MyAccountManagerService extends AccountManagerService { - private Context mRealTestContext; - MyAccountManagerService(Context context, PackageManager packageManager, - IAccountAuthenticatorCache authenticatorCache, Context realTestContext) { - super(context, packageManager, authenticatorCache); - this.mRealTestContext = realTestContext; + Looper getMessageHandlerLooper() { + return Looper.getMainLooper(); } @Override - protected void installNotification(final int notificationId, final Notification n, UserHandle user) { + void addLocalService(AccountManagerInternal service) { } @Override - protected void cancelNotification(final int id, UserHandle user) { + IAccountAuthenticatorCache getAccountAuthenticatorCache() { + return new MockAccountAuthenticatorCache(); } @Override protected String getCeDatabaseName(int userId) { - return new File(mRealTestContext.getCacheDir(), CE_DB).getPath(); + return new File(mRealContext.getCacheDir(), CE_DB).getPath(); } @Override protected String getDeDatabaseName(int userId) { - return new File(mRealTestContext.getCacheDir(), DE_DB).getPath(); + return new File(mRealContext.getCacheDir(), DE_DB).getPath(); } @Override String getPreNDatabaseName(int userId) { - return new File(mRealTestContext.getCacheDir(), PREN_DB).getPath(); + return new File(mRealContext.getCacheDir(), PREN_DB).getPath(); + } + + @Override + INotificationManager getNotificationManager() { + return mock(INotificationManager.class); } } } diff --git a/services/tests/servicestests/src/com/android/server/am/TaskStackChangedListenerTest.java b/services/tests/servicestests/src/com/android/server/am/TaskStackChangedListenerTest.java new file mode 100644 index 000000000000..b47d17efef70 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/am/TaskStackChangedListenerTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.am; + +import static android.support.test.InstrumentationRegistry.getInstrumentation; + +import android.app.Activity; +import android.app.ActivityManagerNative; +import android.app.IActivityManager; +import android.app.ITaskStackListener; +import android.content.Context; +import android.content.Intent; +import android.os.RemoteException; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.MediumTest; +import android.support.test.runner.AndroidJUnit4; +import android.support.test.uiautomator.UiDevice; + +import com.android.internal.annotations.GuardedBy; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@MediumTest +@RunWith(AndroidJUnit4.class) +public class TaskStackChangedListenerTest extends ITaskStackListener.Stub { + + private IActivityManager mService; + + private static final Object sLock = new Object(); + @GuardedBy("sLock") + private static boolean sTaskStackChangedCalled; + private static boolean sActivityBResumed; + + @Before + public void setUp() throws Exception { + mService = ActivityManagerNative.getDefault(); + mService.registerTaskStackListener(this); + } + + @Test + public void testTaskStackChanged_afterFinish() throws Exception { + Context ctx = InstrumentationRegistry.getContext(); + ctx.startActivity(new Intent(ctx, ActivityA.class)); + UiDevice.getInstance(getInstrumentation()).waitForIdle(); + synchronized (sLock) { + Assert.assertTrue(sTaskStackChangedCalled); + } + Assert.assertTrue(sActivityBResumed); + } + + @Override + public void onTaskStackChanged() throws RemoteException { + synchronized (sLock) { + sTaskStackChangedCalled = true; + } + } + + @Override + public void onActivityPinned() throws RemoteException { + } + + @Override + public void onPinnedActivityRestartAttempt() throws RemoteException { + } + + @Override + public void onPinnedStackAnimationEnded() throws RemoteException { + } + + @Override + public void onActivityForcedResizable(String packageName, int taskId) throws RemoteException { + } + + @Override + public void onActivityDismissingDockedStack() throws RemoteException { + } + + public static class ActivityA extends Activity { + + private boolean mActivityBLaunched = false; + + @Override + protected void onPostResume() { + super.onPostResume(); + if (mActivityBLaunched) { + return; + } + mActivityBLaunched = true; + finish(); + startActivity(new Intent(this, ActivityB.class)); + } + } + + public static class ActivityB extends Activity { + + @Override + protected void onPostResume() { + super.onPostResume(); + synchronized (sLock) { + sTaskStackChangedCalled = false; + } + sActivityBResumed = true; + finish(); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest7.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest7.java index 3c99174142cb..25f9100d9252 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest7.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest7.java @@ -68,7 +68,7 @@ public class ShortcutManagerTest7 extends BaseShortcutManagerTest { /* fdin*/ null, /* fdout*/ fd.getFileDescriptor(), /* fderr*/ fd.getFileDescriptor(), - args, rr); + args, null, rr); } return readAll(out); } finally { diff --git a/services/tests/servicestests/src/com/android/server/retaildemo/RetailDemoModeServiceTest.java b/services/tests/servicestests/src/com/android/server/retaildemo/RetailDemoModeServiceTest.java index 56a170c5cc0e..da4d15048c55 100644 --- a/services/tests/servicestests/src/com/android/server/retaildemo/RetailDemoModeServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/retaildemo/RetailDemoModeServiceTest.java @@ -46,6 +46,7 @@ import android.content.pm.UserInfo; import android.content.res.Configuration; import android.media.AudioManager; import android.net.Uri; +import android.net.wifi.WifiManager; import android.os.FileUtils; import android.os.Handler; import android.os.Looper; @@ -62,7 +63,6 @@ import android.util.ArrayMap; import com.android.internal.util.FakeSettingsProvider; import com.android.internal.widget.LockPatternUtils; -import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.retaildemo.RetailDemoModeService.Injector; @@ -75,7 +75,6 @@ import org.junit.runners.JUnit4; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.io.File; @@ -97,6 +96,7 @@ public class RetailDemoModeServiceTest { private @Mock NotificationManager mNm; private @Mock ActivityManagerInternal mAmi; private @Mock AudioManager mAudioManager; + private @Mock WifiManager mWifiManager; private @Mock LockPatternUtils mLockPatternUtils; private MockPreloadAppsInstaller mPreloadAppsInstaller; private MockContentResolver mContentResolver; @@ -135,9 +135,6 @@ public class RetailDemoModeServiceTest { @After public void tearDown() { - // Remove the RetailDemoModeServiceInternal from LocalServices which would've been - // added during initialization of RetailDemoModeService in setUp(). - LocalServices.removeServiceForTest(RetailDemoModeServiceInternal.class); FileUtils.deleteContentsAndDir(mTestPreloadsDir); } @@ -145,22 +142,21 @@ public class RetailDemoModeServiceTest { public void testDemoUserSetup() throws Exception { mService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START); - final ArgumentCaptor<IntentFilter> intentFilter = - ArgumentCaptor.forClass(IntentFilter.class); - verify(mContext).registerReceiver(any(BroadcastReceiver.class), intentFilter.capture()); - assertTrue("Not registered for " + Intent.ACTION_SCREEN_OFF, - intentFilter.getValue().hasAction(Intent.ACTION_SCREEN_OFF)); - mLatch = new CountDownLatch(1); + final UserInfo userInfo = new UserInfo(); + userInfo.id = TEST_DEMO_USER; + when(mUm.createUser(anyString(), anyInt())).thenReturn(userInfo); mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); assertEquals(SYSTEM_PROPERTY_RETAIL_DEMO_ENABLED + " property not set", "1", mInjector.systemPropertiesGet(SYSTEM_PROPERTY_RETAIL_DEMO_ENABLED)); - final UserInfo userInfo = new UserInfo(); - userInfo.id = TEST_DEMO_USER; - when(mUm.createUser(anyString(), anyInt())).thenReturn(userInfo); - mInjector.setDemoUserId(TEST_DEMO_USER); + final ArgumentCaptor<IntentFilter> intentFilter = + ArgumentCaptor.forClass(IntentFilter.class); + verify(mContext).registerReceiver(any(BroadcastReceiver.class), intentFilter.capture()); + assertTrue("Not registered for " + Intent.ACTION_SCREEN_OFF, + intentFilter.getValue().hasAction(Intent.ACTION_SCREEN_OFF)); + setCameraPackage(TEST_CAMERA_PKG); // Wait for the setup to complete. mLatch.await(SETUP_COMPLETE_TIMEOUT_MS, TimeUnit.MILLISECONDS); @@ -197,19 +193,28 @@ public class RetailDemoModeServiceTest { } @Test - public void testSettingsObserver() throws Exception { + public void testSettingsObserver_disableDemoMode() throws Exception { final RetailDemoModeService.SettingsObserver observer = mService.new SettingsObserver(new Handler(Looper.getMainLooper())); final Uri deviceDemoModeUri = Settings.Global.getUriFor(Settings.Global.DEVICE_DEMO_MODE); + when(mUm.hasUserRestriction(UserManager.DISALLOW_SAFE_BOOT, UserHandle.SYSTEM)) + .thenReturn(false); + Settings.Global.putInt(mContentResolver, Settings.Global.PACKAGE_VERIFIER_ENABLE, 1); // Settings.Global.DEVICE_DEMO_MODE has been set to 1 initially. observer.onChange(false, deviceDemoModeUri); - assertEquals(SYSTEM_PROPERTY_RETAIL_DEMO_ENABLED + " property not set", - "1", mInjector.systemPropertiesGet(SYSTEM_PROPERTY_RETAIL_DEMO_ENABLED)); + final ArgumentCaptor<BroadcastReceiver> receiver = + ArgumentCaptor.forClass(BroadcastReceiver.class); + verify(mContext).registerReceiver(receiver.capture(), any(IntentFilter.class)); + Settings.Global.putInt(mContentResolver, Settings.Global.PACKAGE_VERIFIER_ENABLE, 0); new File(mTestPreloadsDir, "dir1").mkdirs(); new File(mTestPreloadsDir, "file1").createNewFile(); Settings.Global.putInt(mContentResolver, Settings.Global.DEVICE_DEMO_MODE, 0); observer.onChange(false, deviceDemoModeUri); + verify(mContext).unregisterReceiver(receiver.getValue()); + verify(mUm).setUserRestriction(UserManager.DISALLOW_SAFE_BOOT, false, UserHandle.SYSTEM); + assertEquals("Package verifier enable value has not been reset", 1, + Settings.Global.getInt(mContentResolver, Settings.Global.PACKAGE_VERIFIER_ENABLE)); Thread.sleep(20); // Wait for the deletion to complete. // verify that the preloaded directory is emptied. assertEquals("Preloads directory is not emptied", @@ -217,6 +222,23 @@ public class RetailDemoModeServiceTest { } @Test + public void testSettingsObserver_enableDemoMode() throws Exception { + final RetailDemoModeService.SettingsObserver observer = + mService.new SettingsObserver(new Handler(Looper.getMainLooper())); + final Uri deviceDemoModeUri = Settings.Global.getUriFor(Settings.Global.DEVICE_DEMO_MODE); + // Settings.Global.DEVICE_DEMO_MODE has been set to 1 initially. + observer.onChange(false, deviceDemoModeUri); + assertEquals(SYSTEM_PROPERTY_RETAIL_DEMO_ENABLED + " property not set", + "1", mInjector.systemPropertiesGet(SYSTEM_PROPERTY_RETAIL_DEMO_ENABLED)); + + final ArgumentCaptor<IntentFilter> intentFilter = + ArgumentCaptor.forClass(IntentFilter.class); + verify(mContext).registerReceiver(any(BroadcastReceiver.class), intentFilter.capture()); + assertTrue("Not registered for " + Intent.ACTION_SCREEN_OFF, + intentFilter.getValue().hasAction(Intent.ACTION_SCREEN_OFF)); + } + + @Test public void testSwitchToDemoUser() { // To make the RetailDemoModeService update it's internal state. mService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START); @@ -227,6 +249,7 @@ public class RetailDemoModeServiceTest { final UserInfo userInfo = new UserInfo(TEST_DEMO_USER, "demo_user", UserInfo.FLAG_DEMO | UserInfo.FLAG_EPHEMERAL); when(mUm.getUserInfo(TEST_DEMO_USER)).thenReturn(userInfo); + when(mWifiManager.isWifiEnabled()).thenReturn(false); final int minVolume = -111; for (int stream : RetailDemoModeService.VOLUME_STREAMS_TO_MUTE) { when(mAudioManager.getStreamMinVolume(stream)).thenReturn(minVolume); @@ -238,6 +261,7 @@ public class RetailDemoModeServiceTest { verify(mAudioManager).setStreamVolume(stream, minVolume, 0); } verify(mLockPatternUtils).setLockScreenDisabled(true, TEST_DEMO_USER); + verify(mWifiManager).setWifiEnabled(true); } private void setCameraPackage(String pkgName) { @@ -304,7 +328,6 @@ public class RetailDemoModeServiceTest { private class TestInjector extends Injector { private ArrayMap<String, String> mSystemProperties = new ArrayMap<>(); - private int mDemoUserId = UserHandle.USER_NULL; TestInjector() { super(mContext); @@ -321,6 +344,11 @@ public class RetailDemoModeServiceTest { } @Override + WifiManager getWifiManager() { + return mWifiManager; + } + + @Override void switchUser(int userId) { if (mLatch != null) { mLatch.countDown(); @@ -376,6 +404,10 @@ public class RetailDemoModeServiceTest { } @Override + void destroyWakeLock() { + } + + @Override boolean isWakeLockHeld() { return false; } @@ -416,12 +448,13 @@ public class RetailDemoModeServiceTest { return mTestPreloadsDir; } - String systemPropertiesGet(String key) { - return mSystemProperties.get(key); + @Override + void publishLocalService(RetailDemoModeService service, + RetailDemoModeServiceInternal localService) { } - void setDemoUserId(int userId) { - mDemoUserId = userId; + String systemPropertiesGet(String key) { + return mSystemProperties.get(key); } } } diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java index eb2372a03a96..6eb347ba7444 100644 --- a/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java +++ b/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java @@ -19,6 +19,7 @@ package com.android.server.wm; import org.junit.Test; import org.junit.runner.RunWith; +import android.content.res.Configuration; import android.platform.test.annotations.Presubmit; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -28,6 +29,8 @@ import java.util.Comparator; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static org.junit.Assert.assertEquals; @@ -377,6 +380,160 @@ public class WindowContainerTests { assertEquals(1, child2223.compareTo(child21)); } + @Test + public void testConfigurationInit() throws Exception { + final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(); + + // Check root container initial config. + final TestWindowContainer root = builder.setLayer(0).build(); + assertEquals(Configuration.EMPTY, root.getOverrideConfiguration()); + assertEquals(Configuration.EMPTY, root.getMergedOverrideConfiguration()); + assertEquals(Configuration.EMPTY, root.getConfiguration()); + + // Check child initial config. + final TestWindowContainer child1 = root.addChildWindow(); + assertEquals(Configuration.EMPTY, child1.getOverrideConfiguration()); + assertEquals(Configuration.EMPTY, child1.getMergedOverrideConfiguration()); + assertEquals(Configuration.EMPTY, child1.getConfiguration()); + + // Check child initial config if root has overrides. + final Configuration rootOverrideConfig = new Configuration(); + rootOverrideConfig.fontScale = 1.3f; + root.onOverrideConfigurationChanged(rootOverrideConfig); + final TestWindowContainer child2 = root.addChildWindow(); + assertEquals(Configuration.EMPTY, child2.getOverrideConfiguration()); + assertEquals(rootOverrideConfig, child2.getMergedOverrideConfiguration()); + assertEquals(rootOverrideConfig, child2.getConfiguration()); + + // Check child initial config if root has parent config set. + final Configuration rootParentConfig = new Configuration(); + rootParentConfig.fontScale = 0.8f; + rootParentConfig.orientation = SCREEN_ORIENTATION_LANDSCAPE; + root.onConfigurationChanged(rootParentConfig); + final Configuration rootFullConfig = new Configuration(rootParentConfig); + rootFullConfig.updateFrom(rootOverrideConfig); + + final TestWindowContainer child3 = root.addChildWindow(); + assertEquals(Configuration.EMPTY, child3.getOverrideConfiguration()); + assertEquals(rootOverrideConfig, child3.getMergedOverrideConfiguration()); + assertEquals(rootFullConfig, child3.getConfiguration()); + } + + @Test + public void testConfigurationChangeOnAddRemove() throws Exception { + final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(); + + // Init root's config. + final TestWindowContainer root = builder.setLayer(0).build(); + final Configuration rootOverrideConfig = new Configuration(); + rootOverrideConfig.fontScale = 1.3f; + root.onOverrideConfigurationChanged(rootOverrideConfig); + + // Init child's config. + final TestWindowContainer child = root.addChildWindow(); + final Configuration childOverrideConfig = new Configuration(); + childOverrideConfig.densityDpi = 320; + child.onOverrideConfigurationChanged(childOverrideConfig); + + // Check configuration update when child is removed from parent. + root.removeChild(child); + assertEquals(childOverrideConfig, child.getOverrideConfiguration()); + assertEquals(childOverrideConfig, child.getMergedOverrideConfiguration()); + assertEquals(childOverrideConfig, child.getConfiguration()); + + // It may be paranoia... but let's check if parent's config didn't change after removal. + assertEquals(rootOverrideConfig, root.getOverrideConfiguration()); + assertEquals(rootOverrideConfig, root.getMergedOverrideConfiguration()); + assertEquals(rootOverrideConfig, root.getConfiguration()); + + // Check configuration update when child is added to parent. + final Configuration mergedOverrideConfig = new Configuration(root.getConfiguration()); + mergedOverrideConfig.updateFrom(childOverrideConfig); + root.addChildWindow(child); + assertEquals(childOverrideConfig, child.getOverrideConfiguration()); + assertEquals(mergedOverrideConfig, child.getMergedOverrideConfiguration()); + assertEquals(mergedOverrideConfig, child.getConfiguration()); + } + + @Test + public void testConfigurationChangePropagation() throws Exception { + final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(); + + // Builds 3-level vertical hierarchy with one window container on each level. + // In addition to different overrides on each level, everyone in hierarchy will have one + // common overridden value - orientation; + + // Init root's config. + final TestWindowContainer root = builder.setLayer(0).build(); + final Configuration rootOverrideConfig = new Configuration(); + rootOverrideConfig.fontScale = 1.3f; + rootOverrideConfig.orientation = SCREEN_ORIENTATION_REVERSE_LANDSCAPE; + root.onOverrideConfigurationChanged(rootOverrideConfig); + + // Init children. + final TestWindowContainer child1 = root.addChildWindow(); + final Configuration childOverrideConfig1 = new Configuration(); + childOverrideConfig1.densityDpi = 320; + childOverrideConfig1.orientation = SCREEN_ORIENTATION_LANDSCAPE; + child1.onOverrideConfigurationChanged(childOverrideConfig1); + + final TestWindowContainer child2 = child1.addChildWindow(); + final Configuration childOverrideConfig2 = new Configuration(); + childOverrideConfig2.screenWidthDp = 150; + childOverrideConfig2.orientation = SCREEN_ORIENTATION_PORTRAIT; + child2.onOverrideConfigurationChanged(childOverrideConfig2); + + // Check configuration on all levels when root override is updated. + rootOverrideConfig.smallestScreenWidthDp = 200; + root.onOverrideConfigurationChanged(rootOverrideConfig); + + final Configuration mergedOverrideConfig1 = new Configuration(rootOverrideConfig); + mergedOverrideConfig1.updateFrom(childOverrideConfig1); + final Configuration mergedConfig1 = new Configuration(mergedOverrideConfig1); + + final Configuration mergedOverrideConfig2 = new Configuration(mergedOverrideConfig1); + mergedOverrideConfig2.updateFrom(childOverrideConfig2); + final Configuration mergedConfig2 = new Configuration(mergedOverrideConfig2); + + assertEquals(rootOverrideConfig, root.getOverrideConfiguration()); + assertEquals(rootOverrideConfig, root.getMergedOverrideConfiguration()); + assertEquals(rootOverrideConfig, root.getConfiguration()); + + assertEquals(childOverrideConfig1, child1.getOverrideConfiguration()); + assertEquals(mergedOverrideConfig1, child1.getMergedOverrideConfiguration()); + assertEquals(mergedConfig1, child1.getConfiguration()); + + assertEquals(childOverrideConfig2, child2.getOverrideConfiguration()); + assertEquals(mergedOverrideConfig2, child2.getMergedOverrideConfiguration()); + assertEquals(mergedConfig2, child2.getConfiguration()); + + // Check configuration on all levels when root parent config is updated. + final Configuration rootParentConfig = new Configuration(); + rootParentConfig.screenHeightDp = 100; + rootParentConfig.orientation = SCREEN_ORIENTATION_REVERSE_PORTRAIT; + root.onConfigurationChanged(rootParentConfig); + final Configuration mergedRootConfig = new Configuration(rootParentConfig); + mergedRootConfig.updateFrom(rootOverrideConfig); + + mergedConfig1.setTo(mergedRootConfig); + mergedConfig1.updateFrom(mergedOverrideConfig1); + + mergedConfig2.setTo(mergedConfig1); + mergedConfig2.updateFrom(mergedOverrideConfig2); + + assertEquals(rootOverrideConfig, root.getOverrideConfiguration()); + assertEquals(rootOverrideConfig, root.getMergedOverrideConfiguration()); + assertEquals(mergedRootConfig, root.getConfiguration()); + + assertEquals(childOverrideConfig1, child1.getOverrideConfiguration()); + assertEquals(mergedOverrideConfig1, child1.getMergedOverrideConfiguration()); + assertEquals(mergedConfig1, child1.getConfiguration()); + + assertEquals(childOverrideConfig2, child2.getOverrideConfiguration()); + assertEquals(mergedOverrideConfig2, child2.getMergedOverrideConfiguration()); + assertEquals(mergedConfig2, child2.getConfiguration()); + } + /* Used so we can gain access to some protected members of the {@link WindowContainer} class */ private class TestWindowContainer extends WindowContainer<TestWindowContainer> { private final int mLayer; diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 411d881c2dde..deed6d4fee78 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -182,6 +182,13 @@ public class CarrierConfigManager { public static final String KEY_SUPPORT_SWAP_AFTER_MERGE_BOOL = "support_swap_after_merge_bool"; /** + * Since the default voicemail number is empty, if a SIM card does not have a voicemail number + * available the user cannot use voicemail. This flag allows the user to edit the voicemail + * number in such cases, and is false by default. + */ + public static final String KEY_EDITABLE_VOICEMAIL_NUMBER_BOOL= "editable_voicemail_number_bool"; + + /** * Determine whether the voicemail notification is persistent in the notification bar. If true, * the voicemail notifications cannot be dismissed from the notification bar. */ @@ -312,13 +319,23 @@ public class CarrierConfigManager { "carrier_wfc_supports_wifi_only_bool"; /** - * Default WFC_IMS_mode 0: WIFI_ONLY - * 1: CELLULAR_PREFERRED - * 2: WIFI_PREFERRED + * Default WFC_IMS_MODE for home network 0: WIFI_ONLY + * 1: CELLULAR_PREFERRED + * 2: WIFI_PREFERRED * @hide */ public static final String KEY_CARRIER_DEFAULT_WFC_IMS_MODE_INT = "carrier_default_wfc_ims_mode_int"; + + /** + * Default WFC_IMS_MODE for roaming + * See {@link KEY_CARRIER_DEFAULT_WFC_IMS_MODE_INT} for valid values. + * + * @hide + */ + public static final String KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_MODE_INT = + "carrier_default_wfc_ims_roaming_mode_int"; + /** * Default WFC_IMS_enabled: true VoWiFi by default is on * false VoWiFi by default is off @@ -1011,6 +1028,18 @@ public class CarrierConfigManager { */ public static final String KEY_RCS_CONFIG_SERVER_URL_STRING = "rcs_config_server_url_string"; + /** + * Determine whether user can change Wi-Fi Calling preference in roaming. + * {@code false} - roaming preference {@link KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_MODE_INT} is + * the same as home preference {@link KEY_CARRIER_DEFAULT_WFC_IMS_MODE_INT} + * and cannot be changed. + * {@code true} - roaming preference can be changed by user independently. + * + * @hide + */ + public static final String KEY_EDITABLE_WFC_ROAMING_MODE_BOOL = + "editable_wfc_roaming_mode_bool"; + /** The default value for every variable. */ private final static PersistableBundle sDefaults; @@ -1034,6 +1063,7 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL, false); sDefaults.putBoolean(KEY_CARRIER_PROMOTE_WFC_ON_CALL_FAIL_BOOL, false); sDefaults.putInt(KEY_CARRIER_DEFAULT_WFC_IMS_MODE_INT, 2); + sDefaults.putInt(KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_MODE_INT, 2); sDefaults.putBoolean(KEY_CARRIER_FORCE_DISABLE_ETWS_CMAS_TEST_BOOL, false); sDefaults.putBoolean(KEY_CARRIER_VOLTE_PROVISIONING_REQUIRED_BOOL, false); sDefaults.putBoolean(KEY_CARRIER_VOLTE_TTY_SUPPORTED_BOOL, true); @@ -1061,6 +1091,7 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_SUPPORT_PAUSE_IMS_VIDEO_CALLS_BOOL, false); sDefaults.putBoolean(KEY_SUPPORT_SWAP_AFTER_MERGE_BOOL, true); sDefaults.putBoolean(KEY_USE_HFA_FOR_PROVISIONING_BOOL, false); + sDefaults.putBoolean(KEY_EDITABLE_VOICEMAIL_NUMBER_BOOL, false); sDefaults.putBoolean(KEY_USE_OTASP_FOR_PROVISIONING_BOOL, false); sDefaults.putBoolean(KEY_VOICEMAIL_NOTIFICATION_PERSISTENT_BOOL, false); sDefaults.putBoolean(KEY_VOICE_PRIVACY_DISABLE_UI_BOOL, false); @@ -1189,6 +1220,7 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_ENHANCED_4G_LTE_TITLE_VARIANT_BOOL, false); sDefaults.putBoolean(KEY_NOTIFY_VT_HANDOVER_TO_WIFI_FAILURE_BOOL, false); sDefaults.putStringArray(FILTERED_CNAP_NAMES_STRING_ARRAY, null); + sDefaults.putBoolean(KEY_EDITABLE_WFC_ROAMING_MODE_BOOL, false); } /** diff --git a/tests/VoiceInteraction/AndroidManifest.xml b/tests/VoiceInteraction/AndroidManifest.xml index cbc6c76aa934..5fdf0dd3992c 100644 --- a/tests/VoiceInteraction/AndroidManifest.xml +++ b/tests/VoiceInteraction/AndroidManifest.xml @@ -1,6 +1,8 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.test.voiceinteraction"> + <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="25" /> + <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" /> <uses-permission android:name="android.permission.READ_LOGS" /> diff --git a/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java index f1da3a266448..bcfe3bffae17 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java @@ -627,7 +627,7 @@ public final class Bitmap_Delegate { boolean isPremultiplied = createFlags.contains(BitmapCreateFlags.PREMULTIPLIED); // and create/return a new Bitmap with it - return new Bitmap(nativeInt, null /* buffer */, width, height, density, isMutable, + return new Bitmap(nativeInt, width, height, density, isMutable, isPremultiplied, null /*ninePatchChunk*/, null /* layoutBounds */); } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java index 8d93b7f32100..a0b297701869 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java @@ -76,6 +76,7 @@ import android.os.Parcel; import android.os.PowerManager; import android.os.RemoteException; import android.os.ResultReceiver; +import android.os.ShellCallback; import android.os.UserHandle; import android.util.AttributeSet; import android.util.DisplayMetrics; @@ -1180,7 +1181,7 @@ public final class BridgeContext extends Context { @Override public void shellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, - String[] args, ResultReceiver resultReceiver) { + String[] args, ShellCallback shellCallback, ResultReceiver resultReceiver) { } }; } diff --git a/wifi/java/android/net/wifi/ScanResult.java b/wifi/java/android/net/wifi/ScanResult.java index 67cf10732b9d..465addfc1469 100644 --- a/wifi/java/android/net/wifi/ScanResult.java +++ b/wifi/java/android/net/wifi/ScanResult.java @@ -139,12 +139,6 @@ public class ScanResult implements Parcelable { public long seen; /** - * If the scan result is a valid autojoin candidate - * {@hide} - */ - public int isAutoJoinCandidate; - - /** * @hide * Update RSSI of the scan result * @param previousRssi @@ -452,7 +446,6 @@ public class ScanResult implements Parcelable { numConnection = source.numConnection; numUsage = source.numUsage; numIpConfigFailures = source.numIpConfigFailures; - isAutoJoinCandidate = source.isAutoJoinCandidate; venueName = source.venueName; operatorFriendlyName = source.operatorFriendlyName; flags = source.flags; @@ -530,7 +523,6 @@ public class ScanResult implements Parcelable { dest.writeInt(numConnection); dest.writeInt(numUsage); dest.writeInt(numIpConfigFailures); - dest.writeInt(isAutoJoinCandidate); dest.writeString((venueName != null) ? venueName.toString() : ""); dest.writeString((operatorFriendlyName != null) ? operatorFriendlyName.toString() : ""); dest.writeLong(this.flags); @@ -600,7 +592,6 @@ public class ScanResult implements Parcelable { sr.numConnection = in.readInt(); sr.numUsage = in.readInt(); sr.numIpConfigFailures = in.readInt(); - sr.isAutoJoinCandidate = in.readInt(); sr.venueName = in.readString(); sr.operatorFriendlyName = in.readString(); sr.flags = in.readLong(); diff --git a/wifi/java/android/net/wifi/nan/PublishConfig.java b/wifi/java/android/net/wifi/nan/PublishConfig.java index 91513713daa3..4b67f9ac0ef9 100644 --- a/wifi/java/android/net/wifi/nan/PublishConfig.java +++ b/wifi/java/android/net/wifi/nan/PublishConfig.java @@ -257,25 +257,6 @@ public final class PublishConfig implements Parcelable { } /** - * Specify service specific information for the publish session - a simple wrapper - * of {@link PublishConfig.Builder#setServiceSpecificInfo(byte[])} - * obtaining the data from a String. - * <p> - * Optional. Empty by default. - * - * @param serviceSpecificInfoStr The service specific information string - * to be included (as a byte array) in the publish - * information. - * - * @return The builder to facilitate chaining - * {@code builder.setXXX(..).setXXX(..)}. - */ - public Builder setServiceSpecificInfo(@NonNull String serviceSpecificInfoStr) { - mServiceSpecificInfo = serviceSpecificInfoStr.getBytes(); - return this; - } - - /** * The match filter for a publish session. Used to determine whether a service * discovery occurred - in addition to relying on the service name. * <p> diff --git a/wifi/java/android/net/wifi/nan/SubscribeConfig.java b/wifi/java/android/net/wifi/nan/SubscribeConfig.java index b1dcd8fe49d0..4352fcf6a3cf 100644 --- a/wifi/java/android/net/wifi/nan/SubscribeConfig.java +++ b/wifi/java/android/net/wifi/nan/SubscribeConfig.java @@ -289,25 +289,6 @@ public final class SubscribeConfig implements Parcelable { } /** - * Specify service specific information for the subscribe session - a simple wrapper - * of {@link SubscribeConfig.Builder#setServiceSpecificInfo(byte[])} - * obtaining the data from a String. - * <p> - * Optional. Empty by default. - * - * @param serviceSpecificInfoStr The service specific information string - * to be included (as a byte array) in the subscribe - * information. - * - * @return The builder to facilitate chaining - * {@code builder.setXXX(..).setXXX(..)}. - */ - public Builder setServiceSpecificInfo(@NonNull String serviceSpecificInfoStr) { - mServiceSpecificInfo = serviceSpecificInfoStr.getBytes(); - return this; - } - - /** * The match filter for a subscribe session. Used to determine whether a service * discovery occurred - in addition to relying on the service name. * <p> diff --git a/wifi/java/android/net/wifi/nan/WifiNanManager.java b/wifi/java/android/net/wifi/nan/WifiNanManager.java index 705ba4a5a4bd..bb1543418c6d 100644 --- a/wifi/java/android/net/wifi/nan/WifiNanManager.java +++ b/wifi/java/android/net/wifi/nan/WifiNanManager.java @@ -201,6 +201,8 @@ public class WifiNanManager { * Use the {@link #isAvailable()} to query the current status. * This broadcast is <b>not</b> sticky, use the {@link #isAvailable()} API after registering * the broadcast to check the current state of Wi-Fi NAN. + * <p>Note: The broadcast is only delivered to registered receivers - no manifest registered + * components will be launched. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_WIFI_NAN_STATE_CHANGED = diff --git a/wifi/tests/Android.mk b/wifi/tests/Android.mk new file mode 100644 index 000000000000..5850feee004a --- /dev/null +++ b/wifi/tests/Android.mk @@ -0,0 +1,61 @@ +# Copyright (C) 2016 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOCAL_PATH:= $(call my-dir) + +# Make test APK +# ============================================================ +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +# This list is generated from the java source files in this module +# The list is a comma separated list of class names with * matching zero or more characters. +# Example: +# Input files: src/com/android/server/wifi/Test.java src/com/android/server/wifi/AnotherTest.java +# Generated exclude list: com.android.server.wifi.Test*,com.android.server.wifi.AnotherTest* + +# Filter all src files to just java files +local_java_files := $(filter %.java,$(LOCAL_SRC_FILES)) +# Transform java file names into full class names. +# This only works if the class name matches the file name and the directory structure +# matches the package. +local_classes := $(subst /,.,$(patsubst src/%.java,%,$(local_java_files))) +# Utility variables to allow replacing a space with a comma +comma:= , +empty:= +space:= $(empty) $(empty) +# Convert class name list to jacoco exclude list +# This appends a * to all classes and replace the space separators with commas. +# These patterns will match all classes in this module and their inner classes. +jacoco_exclude := $(subst $(space),$(comma),$(patsubst %,%*,$(local_classes))) + +jacoco_include := android.net.wifi.* + +LOCAL_JACK_COVERAGE_INCLUDE_FILTER := $(jacoco_include) +LOCAL_JACK_COVERAGE_EXCLUDE_FILTER := $(jacoco_exclude) + +LOCAL_STATIC_JAVA_LIBRARIES := \ + android-support-test \ + mockito-target-minus-junit4 \ + frameworks-base-testutils \ + +LOCAL_JAVA_LIBRARIES := \ + android.test.runner \ + +LOCAL_PACKAGE_NAME := FrameworksWifiApiTests + +include $(BUILD_PACKAGE) diff --git a/wifi/tests/AndroidManifest.xml b/wifi/tests/AndroidManifest.xml new file mode 100644 index 000000000000..4eaca2b98a1d --- /dev/null +++ b/wifi/tests/AndroidManifest.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ Copyright (C) 2016 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android.net.wifi.test"> + + <application> + <uses-library android:name="android.test.runner" /> + <activity android:label="WifiTestDummyLabel" + android:name="WifiTestDummyName"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + </application> + + <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner" + android:targetPackage="android.net.wifi.test" + android:label="Frameworks Wifi API Tests"> + </instrumentation> + +</manifest> diff --git a/wifi/tests/README.md b/wifi/tests/README.md new file mode 100644 index 000000000000..b418abd5e46b --- /dev/null +++ b/wifi/tests/README.md @@ -0,0 +1,50 @@ +# Wifi Unit Tests +This package contains unit tests for the android wifi framework APIs based on the +[Android Testing Support Library](http://developer.android.com/tools/testing-support-library/index.html). +The test cases are built using the [JUnit](http://junit.org/) and [Mockito](http://mockito.org/) +libraries. + +## Running Tests +The easiest way to run tests is simply run + +``` +frameworks/base/wifi/tests/runtests.sh +``` + +`runtests.sh` will build the test project and all of its dependencies and push the APK to the +connected device. It will then run the tests on the device. + +To pick up changes in framework/base, you will need to: +1. rebuild the framework library 'make -j32' +2. sync over the updated library to the device 'adb sync' +3. restart framework on the device 'adb shell stop' then 'adb shell start' + +To enable syncing data to the device for first time after clean reflash: +1. adb disable-verity +2. adb reboot +3. adb remount + +See below for a few example of options to limit which tests are run. +See the +[AndroidJUnitRunner Documentation](https://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner.html) +for more details on the supported options. + +``` +runtests.sh -e package android.net.wifi +runtests.sh -e class android.net.wifi.WifiScannerTest +``` + +If you manually build and push the test APK to the device you can run tests using + +``` +adb shell am instrument -w 'android.net.wifi.test/android.support.test.runner.AndroidJUnitRunner' +``` + +## Adding Tests +Tests can be added by adding classes to the src directory. JUnit4 style test cases can +be written by simply annotating test methods with `org.junit.Test`. + +## Debugging Tests +If you are trying to debug why tests are not doing what you expected, you can add android log +statements and use logcat to view them. The beginning and end of every tests is automatically logged +with the tag `TestRunner`. diff --git a/wifi/tests/runtests.sh b/wifi/tests/runtests.sh new file mode 100755 index 000000000000..ebcc2a2f34a2 --- /dev/null +++ b/wifi/tests/runtests.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +if [ -z $ANDROID_BUILD_TOP ]; then + echo "You need to source and lunch before you can use this script" + exit 1 +fi + +echo "Running tests" + +set -e # fail early + +echo "+ mmma -j32 $ANDROID_BUILD_TOP/frameworks/base/wifi/tests" +# NOTE Don't actually run the command above since this shell doesn't inherit functions from the +# caller. +make -j32 -C $ANDROID_BUILD_TOP -f build/core/main.mk MODULES-IN-frameworks-base-wifi-tests + +set -x # print commands + +adb root +adb wait-for-device + +adb install -r -g "$OUT/data/app/FrameworksWifiApiTests/FrameworksWifiApiTests.apk" + +adb shell am instrument -w "$@" 'android.net.wifi.test/android.support.test.runner.AndroidJUnitRunner' diff --git a/wifi/tests/src/android/net/wifi/FakeKeys.java b/wifi/tests/src/android/net/wifi/FakeKeys.java new file mode 100644 index 000000000000..0c730705b039 --- /dev/null +++ b/wifi/tests/src/android/net/wifi/FakeKeys.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.net.wifi; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +/** + * A class containing test certificates. + */ +public class FakeKeys { + private static final String CA_CERT0_STRING = "-----BEGIN CERTIFICATE-----\n" + + "MIIDKDCCAhCgAwIBAgIJAILlFdwzLVurMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV\n" + + "BAMTB0VBUCBDQTEwHhcNMTYwMTEyMTE1MDE1WhcNMjYwMTA5MTE1MDE1WjASMRAw\n" + + "DgYDVQQDEwdFQVAgQ0ExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\n" + + "znAPUz26Msae4ws43czR41/J2QtrSIZUKmVUsVumDbYHrPNvTXKSMXAcewORDQYX\n" + + "RqvHvpn8CscB1+oGXZvHwxj4zV0WKoK2zeXkau3vcyl3HIKupJfq2TEACefVjj0t\n" + + "JW+X35PGWp9/H5zIUNVNVjS7Ums84IvKhRB8512PB9UyHagXYVX5GWpAcVpyfrlR\n" + + "FI9Qdhh+Pbk0uyktdbf/CdfgHOoebrTtwRljM0oDtX+2Cv6j0wBK7hD8pPvf1+uy\n" + + "GzczigAU/4Kw7eZqydf9B+5RupR+IZipX41xEiIrKRwqi517WWzXcjaG2cNbf451\n" + + "xpH5PnV3i1tq04jMGQUzFwIDAQABo4GAMH4wHQYDVR0OBBYEFIwX4vs8BiBcScod\n" + + "5noZHRM8E4+iMEIGA1UdIwQ7MDmAFIwX4vs8BiBcScod5noZHRM8E4+ioRakFDAS\n" + + "MRAwDgYDVQQDEwdFQVAgQ0ExggkAguUV3DMtW6swDAYDVR0TBAUwAwEB/zALBgNV\n" + + "HQ8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBAFfQqOTA7Rv7K+luQ7pnas4BYwHE\n" + + "9GEP/uohv6KOy0TGQFbrRTjFoLVNB9BZ1ymMDZ0/TIwIUc7wi7a8t5mEqYH153wW\n" + + "aWooiSjyLLhuI4sNrNCOtisdBq2r2MFXt6h0mAQYOPv8R8K7/fgSxGFqzhyNmmVL\n" + + "1qBJldx34SpwsTALQVPb4hGwJzZfr1PcpEQx6xMnTl8xEWZE3Ms99uaUxbQqIwRu\n" + + "LgAOkNCmY2m89VhzaHJ1uV85AdM/tD+Ysmlnnjt9LRCejbBipjIGjOXrg1JP+lxV\n" + + "muM4vH+P/mlmxsPPz0d65b+EGmJZpoLkO/tdNNvCYzjJpTEWpEsO6NMhKYo=\n" + + "-----END CERTIFICATE-----\n"; + public static final X509Certificate CA_CERT0 = loadCertificate(CA_CERT0_STRING); + + private static final String CA_CERT1_STRING = "-----BEGIN CERTIFICATE-----\n" + + "MIIDKDCCAhCgAwIBAgIJAOM5SzKO2pzCMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV\n" + + "BAMTB0VBUCBDQTAwHhcNMTYwMTEyMDAxMDQ3WhcNMjYwMTA5MDAxMDQ3WjASMRAw\n" + + "DgYDVQQDEwdFQVAgQ0EwMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\n" + + "89ug+IEKVQXnJGKg5g4uVHg6J/8iRUxR5k2eH5o03hrJNMfN2D+cBe/wCiZcnWbI\n" + + "GbGZACWm2nQth2wy9Zgm2LOd3b4ocrHYls3XLq6Qb5Dd7a0JKU7pdGufiNVEkrmF\n" + + "EB+N64wgwH4COTvCiN4erp5kyJwkfqAl2xLkZo0C464c9XoyQOXbmYD9A8v10wZu\n" + + "jyNsEo7Nr2USyw+qhjWSbFbEirP77Tvx+7pJQJwdtk1V9Tn73T2dGF2WHYejei9S\n" + + "mcWpdIUqsu9etYH+zDmtu7I1xlkwiaVsNr2+D+qaCJyOYqrDTKVNK5nmbBPXDWZc\n" + + "NoDbTOoqquX7xONpq9M6jQIDAQABo4GAMH4wHQYDVR0OBBYEFAZ3A2S4qJZZwuNY\n" + + "wkJ6mAdc0gVdMEIGA1UdIwQ7MDmAFAZ3A2S4qJZZwuNYwkJ6mAdc0gVdoRakFDAS\n" + + "MRAwDgYDVQQDEwdFQVAgQ0EwggkA4zlLMo7anMIwDAYDVR0TBAUwAwEB/zALBgNV\n" + + "HQ8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBAHmdMwEhtys4d0E+t7owBmoVR+lU\n" + + "hMCcRtWs8YKX5WIM2kTweT0h/O1xwE1mWmRv/IbDAEb8od4BjAQLhIcolStr2JaO\n" + + "9ZzyxjOnNzqeErh/1DHDbb/moPpqfeJ8YiEz7nH/YU56Q8iCPO7TsgS0sNNE7PfN\n" + + "IUsBW0yHRgpQ4OxWmiZG2YZWiECRzAC0ecPzo59N5iH4vLQIMTMYquiDeMPQnn1e\n" + + "NDGxG8gCtDKIaS6tMg3a28MvWB094pr2ETou8O1C8Ji0Y4hE8QJmSdT7I4+GZjgW\n" + + "g94DZ5RiL7sdp3vC48CXOmeT61YBIvhGUsE1rPhXqkpqQ3Z3C4TFF0jXZZc=\n" + + "-----END CERTIFICATE-----\n"; + public static final X509Certificate CA_CERT1 = loadCertificate(CA_CERT1_STRING); + + + private static X509Certificate loadCertificate(String blob) { + try { + final CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + InputStream stream = new ByteArrayInputStream(blob.getBytes(StandardCharsets.UTF_8)); + + return (X509Certificate) certFactory.generateCertificate(stream); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/wifi/tests/src/android/net/wifi/WifiEnterpriseConfigTest.java b/wifi/tests/src/android/net/wifi/WifiEnterpriseConfigTest.java new file mode 100644 index 000000000000..0d964b7c92d7 --- /dev/null +++ b/wifi/tests/src/android/net/wifi/WifiEnterpriseConfigTest.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.net.wifi; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNull; + +import android.net.wifi.WifiEnterpriseConfig.Eap; +import android.net.wifi.WifiEnterpriseConfig.Phase2; +import android.os.Parcel; +import android.security.Credentials; +import android.test.suitebuilder.annotation.SmallTest; + +import org.junit.Before; +import org.junit.Test; + +import java.security.cert.X509Certificate; + + +/** + * Unit tests for {@link android.net.wifi.WifiEnterpriseConfig}. + */ +@SmallTest +public class WifiEnterpriseConfigTest { + // Maintain a ground truth of the keystore uri prefix which is expected by wpa_supplicant. + public static final String KEYSTORE_URI = "keystore://"; + public static final String CA_CERT_PREFIX = KEYSTORE_URI + Credentials.CA_CERTIFICATE; + public static final String KEYSTORES_URI = "keystores://"; + + private WifiEnterpriseConfig mEnterpriseConfig; + + @Before + public void setUp() throws Exception { + mEnterpriseConfig = new WifiEnterpriseConfig(); + } + + @Test + public void testGetEmptyCaCertificate() { + // A newly-constructed WifiEnterpriseConfig object should have no CA certificate. + assertNull(mEnterpriseConfig.getCaCertificate()); + assertNull(mEnterpriseConfig.getCaCertificates()); + // Setting CA certificate to null explicitly. + mEnterpriseConfig.setCaCertificate(null); + assertNull(mEnterpriseConfig.getCaCertificate()); + // Setting CA certificate to null using setCaCertificates(). + mEnterpriseConfig.setCaCertificates(null); + assertNull(mEnterpriseConfig.getCaCertificates()); + // Setting CA certificate to zero-length array. + mEnterpriseConfig.setCaCertificates(new X509Certificate[0]); + assertNull(mEnterpriseConfig.getCaCertificates()); + } + + @Test + public void testSetGetSingleCaCertificate() { + X509Certificate cert0 = FakeKeys.CA_CERT0; + mEnterpriseConfig.setCaCertificate(cert0); + assertEquals(mEnterpriseConfig.getCaCertificate(), cert0); + } + + @Test + public void testSetGetMultipleCaCertificates() { + X509Certificate cert0 = FakeKeys.CA_CERT0; + X509Certificate cert1 = FakeKeys.CA_CERT1; + mEnterpriseConfig.setCaCertificates(new X509Certificate[] {cert0, cert1}); + X509Certificate[] result = mEnterpriseConfig.getCaCertificates(); + assertEquals(result.length, 2); + assertTrue(result[0] == cert0 && result[1] == cert1); + } + + @Test + public void testSaveSingleCaCertificateAlias() { + final String alias = "single_alias 0"; + mEnterpriseConfig.setCaCertificateAliases(new String[] {alias}); + assertEquals(getCaCertField(), CA_CERT_PREFIX + alias); + } + + @Test + public void testLoadSingleCaCertificateAlias() { + final String alias = "single_alias 1"; + setCaCertField(CA_CERT_PREFIX + alias); + String[] aliases = mEnterpriseConfig.getCaCertificateAliases(); + assertEquals(aliases.length, 1); + assertEquals(aliases[0], alias); + } + + @Test + public void testSaveMultipleCaCertificates() { + final String alias0 = "single_alias 0"; + final String alias1 = "single_alias 1"; + mEnterpriseConfig.setCaCertificateAliases(new String[] {alias0, alias1}); + assertEquals(getCaCertField(), String.format("%s%s %s", + KEYSTORES_URI, + WifiEnterpriseConfig.encodeCaCertificateAlias(Credentials.CA_CERTIFICATE + alias0), + WifiEnterpriseConfig.encodeCaCertificateAlias(Credentials.CA_CERTIFICATE + alias1))); + } + + @Test + public void testLoadMultipleCaCertificates() { + final String alias0 = "single_alias 0"; + final String alias1 = "single_alias 1"; + setCaCertField(String.format("%s%s %s", + KEYSTORES_URI, + WifiEnterpriseConfig.encodeCaCertificateAlias(Credentials.CA_CERTIFICATE + alias0), + WifiEnterpriseConfig.encodeCaCertificateAlias(Credentials.CA_CERTIFICATE + alias1))); + String[] aliases = mEnterpriseConfig.getCaCertificateAliases(); + assertEquals(aliases.length, 2); + assertEquals(aliases[0], alias0); + assertEquals(aliases[1], alias1); + } + + private String getCaCertField() { + return mEnterpriseConfig.getFieldValue(WifiEnterpriseConfig.CA_CERT_KEY); + } + + private void setCaCertField(String value) { + mEnterpriseConfig.setFieldValue(WifiEnterpriseConfig.CA_CERT_KEY, value); + } + + // Retrieves the value for a specific key supplied to wpa_supplicant. + private class SupplicantConfigExtractor implements WifiEnterpriseConfig.SupplicantSaver { + private String mValue = null; + private String mKey; + + SupplicantConfigExtractor(String key) { + mKey = key; + } + + @Override + public boolean saveValue(String key, String value) { + if (key.equals(mKey)) { + mValue = value; + } + return true; + } + + public String getValue() { + return mValue; + } + } + + private String getSupplicantEapMethod() { + SupplicantConfigExtractor entryExtractor = new SupplicantConfigExtractor( + WifiEnterpriseConfig.EAP_KEY); + mEnterpriseConfig.saveToSupplicant(entryExtractor); + return entryExtractor.getValue(); + } + + private String getSupplicantPhase2Method() { + SupplicantConfigExtractor entryExtractor = new SupplicantConfigExtractor( + WifiEnterpriseConfig.PHASE2_KEY); + mEnterpriseConfig.saveToSupplicant(entryExtractor); + return entryExtractor.getValue(); + } + + /** Verifies the default value for EAP outer and inner methods */ + @Test + public void eapInnerDefault() { + assertEquals(null, getSupplicantEapMethod()); + assertEquals(null, getSupplicantPhase2Method()); + } + + /** Verifies that the EAP inner method is reset when we switch to TLS */ + @Test + public void eapPhase2MethodForTls() { + // Initially select an EAP method that supports an phase2. + mEnterpriseConfig.setEapMethod(Eap.PEAP); + mEnterpriseConfig.setPhase2Method(Phase2.MSCHAPV2); + assertEquals("PEAP", getSupplicantEapMethod()); + assertEquals("\"auth=MSCHAPV2\"", getSupplicantPhase2Method()); + + // Change the EAP method to another type which supports a phase2. + mEnterpriseConfig.setEapMethod(Eap.TTLS); + assertEquals("TTLS", getSupplicantEapMethod()); + assertEquals("\"auth=MSCHAPV2\"", getSupplicantPhase2Method()); + + // Change the EAP method to TLS which does not support a phase2. + mEnterpriseConfig.setEapMethod(Eap.TLS); + assertEquals(null, getSupplicantPhase2Method()); + } + + /** Verfies that the EAP inner method is reset when we switch phase2 to NONE */ + @Test + public void eapPhase2None() { + // Initially select an EAP method that supports an phase2. + mEnterpriseConfig.setEapMethod(Eap.PEAP); + mEnterpriseConfig.setPhase2Method(Phase2.MSCHAPV2); + assertEquals("PEAP", getSupplicantEapMethod()); + assertEquals("\"auth=MSCHAPV2\"", getSupplicantPhase2Method()); + + // Change the phase2 method to NONE and ensure the value is cleared. + mEnterpriseConfig.setPhase2Method(Phase2.NONE); + assertEquals(null, getSupplicantPhase2Method()); + } + + /** Verfies that the correct "autheap" parameter is supplied for TTLS/GTC. */ + @Test + public void peapGtcToTtls() { + mEnterpriseConfig.setEapMethod(Eap.PEAP); + mEnterpriseConfig.setPhase2Method(Phase2.GTC); + assertEquals("PEAP", getSupplicantEapMethod()); + assertEquals("\"auth=GTC\"", getSupplicantPhase2Method()); + + mEnterpriseConfig.setEapMethod(Eap.TTLS); + assertEquals("TTLS", getSupplicantEapMethod()); + assertEquals("\"autheap=GTC\"", getSupplicantPhase2Method()); + } + + /** Verfies that the correct "auth" parameter is supplied for PEAP/GTC. */ + @Test + public void ttlsGtcToPeap() { + mEnterpriseConfig.setEapMethod(Eap.TTLS); + mEnterpriseConfig.setPhase2Method(Phase2.GTC); + assertEquals("TTLS", getSupplicantEapMethod()); + assertEquals("\"autheap=GTC\"", getSupplicantPhase2Method()); + + mEnterpriseConfig.setEapMethod(Eap.PEAP); + assertEquals("PEAP", getSupplicantEapMethod()); + assertEquals("\"auth=GTC\"", getSupplicantPhase2Method()); + } + + /** Verfies that the copy constructor preseves the inner method information. */ + @Test + public void copyConstructor() { + WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig(); + enterpriseConfig.setEapMethod(Eap.TTLS); + enterpriseConfig.setPhase2Method(Phase2.GTC); + mEnterpriseConfig = new WifiEnterpriseConfig(enterpriseConfig); + assertEquals("TTLS", getSupplicantEapMethod()); + assertEquals("\"autheap=GTC\"", getSupplicantPhase2Method()); + } + + /** Verfies that parceling a WifiEnterpriseConfig preseves method information. */ + @Test + public void parcelConstructor() { + WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig(); + enterpriseConfig.setEapMethod(Eap.TTLS); + enterpriseConfig.setPhase2Method(Phase2.GTC); + Parcel parcel = Parcel.obtain(); + enterpriseConfig.writeToParcel(parcel, 0); + parcel.setDataPosition(0); // Allow parcel to be read from the beginning. + mEnterpriseConfig = WifiEnterpriseConfig.CREATOR.createFromParcel(parcel); + assertEquals("TTLS", getSupplicantEapMethod()); + assertEquals("\"autheap=GTC\"", getSupplicantPhase2Method()); + } + + /** Verifies proper operation of the getKeyId() method. */ + @Test + public void getKeyId() { + assertEquals("NULL", mEnterpriseConfig.getKeyId(null)); + WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig(); + enterpriseConfig.setEapMethod(Eap.TTLS); + enterpriseConfig.setPhase2Method(Phase2.GTC); + assertEquals("TTLS_GTC", mEnterpriseConfig.getKeyId(enterpriseConfig)); + mEnterpriseConfig.setEapMethod(Eap.PEAP); + mEnterpriseConfig.setPhase2Method(Phase2.MSCHAPV2); + assertEquals("PEAP_MSCHAPV2", mEnterpriseConfig.getKeyId(enterpriseConfig)); + } + + /** Verifies that passwords are not displayed in toString. */ + @Test + public void passwordNotInToString() { + String password = "supersecret"; + mEnterpriseConfig.setPassword(password); + assertFalse(mEnterpriseConfig.toString().contains(password)); + } +} diff --git a/wifi/tests/src/android/net/wifi/WifiScannerTest.java b/wifi/tests/src/android/net/wifi/WifiScannerTest.java new file mode 100644 index 000000000000..a829eb933b59 --- /dev/null +++ b/wifi/tests/src/android/net/wifi/WifiScannerTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.wifi; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.validateMockitoUsage; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.net.wifi.WifiScanner.BssidInfo; +import android.net.wifi.WifiScanner.BssidListener; +import android.os.Handler; +import android.os.Message; +import android.os.test.TestLooper; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.internal.util.test.BidirectionalAsyncChannelServer; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit tests for {@link android.net.wifi.WifiScanner}. + */ +@SmallTest +public class WifiScannerTest { + @Mock + private Context mContext; + @Mock + private IWifiScanner mService; + @Mock + private BssidListener mBssidListener; + + private WifiScanner mWifiScanner; + private TestLooper mLooper; + private Handler mHandler; + + /** + * Setup before tests. + */ + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mLooper = new TestLooper(); + mHandler = mock(Handler.class); + BidirectionalAsyncChannelServer server = new BidirectionalAsyncChannelServer( + mContext, mLooper.getLooper(), mHandler); + when(mService.getMessenger()).thenReturn(server.getMessenger()); + mWifiScanner = new WifiScanner(mContext, mService, mLooper.getLooper()); + mLooper.dispatchAll(); + } + + /** + * Clean up after tests. + */ + @After + public void cleanup() { + validateMockitoUsage(); + } + + private void verifySetHotlistMessage(Handler handler) { + ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class); + verify(handler, atLeastOnce()).handleMessage(messageCaptor.capture()); + assertEquals("message.what is not CMD_SET_HOTLIST", + WifiScanner.CMD_SET_HOTLIST, + messageCaptor.getValue().what); + } + + /** + * Test duplicate listeners for bssid tracking. + */ + @Test + public void testStartTrackingBssidsDuplicateListeners() throws Exception { + BssidInfo[] bssids = new BssidInfo[] { + new BssidInfo() + }; + + // First start tracking succeeds. + mWifiScanner.startTrackingBssids(bssids, -100, mBssidListener); + mLooper.dispatchAll(); + verifySetHotlistMessage(mHandler); + + // Second start tracking should fail. + mWifiScanner.startTrackingBssids(bssids, -100, mBssidListener); + mLooper.dispatchAll(); + verify(mBssidListener).onFailure(eq(WifiScanner.REASON_DUPLICATE_REQEUST), anyString()); + } +} |